Pagina1 van 1

MacOS – SMJobBless: Elevated Privileges met Lazarus Pascal

MacOS – SMJobBless: Elevated Privileges met Lazarus Pascal
   0

Zoals sommige al weten, heb ik ooit eens ApplePi-Baker gemaakt, en met de start aan een nieuwe versie wilde ik eigenlijk een betere applicatie bouwen die niet alleen maar een zogenaamde front-end is voor command-line utilities zoals “dd” en “diskutil“. Om dit correct te kunnen doen, moet ik ineens gebruik maken van een zogenaamde Privileged Hulp Programma om root access te krijgen. Daarbij hebben we dan SMJobBless enzo nodig.

Vroeger was het best makkelijk om een backup applicatie te bouwen. Simpel weg de drive openen, lezen en naar een image schrijven. Maar dat kan tegenwoordig niet meer …

Met alle “hack happy” criminelen, moeten bedrijven zoals Apple en Microsoft het steeds veiliger (en dus moeilijker) maken om administratie toegang te krijgen (root access). Uiteraard vind ik een beter veiligheid wenselijk, maar het is wel jammer dat nu meer dan de helft van de tijd die in het ontwikkelen van een programma gaat, gebaseerd is op het aanpassen aan deze nieuwe wijzigingen.

Apple heeft er wel een mechanisme voor, “SMJobBless” of “Privileged Helper“, maar de documentatie is niet al te best. Daarnaast focust Apple natuurlijk alleen maar op Objective C en Swift – welke ik zelf maar prut vindt, en dan hebben we het nog niet over het feit hoe “leuk” ik XCode vindt (not!) – Ik geef de voorkeur aan de taal Pascal en werk dan het liefste in een IDE zoals Lazarus Pascal.

Na heel veel zoeken en uitproberen heb ik het dan eindelijk voor elkaar en in dit artikel beschrijf ik mijn bevindingen.

Let op – Ik ben absoluut geen expert op dit gebied, en probeer je zoveel mogelijk te laten zien van wat ik heb ondervonden. Aangezien, blijkbaar, nog niemand dit gedaan heeft in Lazarus Pascal, zullen er vast punten van verbetering zijn. Laat suggesties en ideëen gerusten horen in de opmerkingen onder het artikel.




Overview

Root Access of te wel Elevated Privileges

Voor je aan dit artikel begint; pak maar wat te drinken en wat te eten – het is een lang artikel …

Waarom hebben we Elevated Privileges nodig?

Het verhaal begint, zoals eerder gezegd, met het maken van een nieuwe versie van ApplePi-Baker, een programma waarmee je een backup en restore van een disk kunt maken.

De eerste versies (1.x) van ApplePi-Baker, is eigenlijk een front-end voor zogenaamde command-line (soort van DOS commando’s) die wel toegang hebben tot een disk, als je maar “sudo” gebruikt. Het was in het begin een handigheidje voor mezelf, maar na meer dan een half miljoen downloads, wilde ik toch een “betere” versie maken. De oude versie werkt overigens prima, en zelfs best vlot. Maar ik wilde het op de juiste manier doen met alles in de applicatie, zonder gebruik te maken van command-line utilities.

Een GUI front-end heeft namelijk een paar potentiële nadelen. De belangrijkste daarbij is dat Apple, met iedere MacOS versie, weer andere output genereerd, waardoor het compatibel houden met de verschillende MacOS versie een drama werd. Er zijn nog wat andere kleine nadelen zoals bruikbaarheid en stabiliteit.

Om volledige toegang te krijgen to een disk heeft men dus “root access” nodig (root/sudo). Iets wat Apple gewoon simpelweg verbiedt in gewone applicaties. Wat je ook probeert – je krijgt het niet aan de gang. De enige uitzondering is dan wel, als je de binary in de app bundle, direct vanuit Terminal met sudo start. Maar ik denk dat we het er wel over eens kunnen zijn dat dit geen handige actie is voor de gemiddelde gebruiker en al helemaal geen plaats heeft in een grafische gebruikersinterface.

Moderne Aanpak: Hoofd Applicatie en Privileged Hulp Programma

Het concept van Apple, om de gebruiker en zijn/haar computer te beschermen, is door de applicatie op te splitsen in twee delen. Een deel handelt de gebruikelijke functies af die geen root access nodig hebben, en een deel wat zich specifiek richt op die ene taak die wel root access nodig heeft.

Dit klinkt allemaal logisch, maar voor een software ontwikkelaar wil dit dus zeggen dat de hij/zij dus minstens 2 programma’s moet ontwikkelen, die vervolgens met elkaar kunnen communiceren.

Zoals gezegd; logisch en een goed concept. Uiteindelijk hoeven we niet alle functies root access te geven. Dit zou een applicatie alleen maar meer kwetsbaar maken voor misbruik.

Dus … de standaard functies gaan in de Hoofd Applicatie. Dit zijn dan vaak functies zoals interactie met de gebruiker etc.

De functies die root access (elevated privileges) nodig hebben, zijn gevaarlijker en makkelijk het doel van een kwaadwillend persoon. Deze functies plaatsen we in een zogenaamde “Helper Tool” of te wel een “Hulp Programma“. Dit Hulp Programmas is in principe een heel klein programma, welke in een veilige omgeving draait en slecht een enkele taak heeft.

Jou applicatie mag meer dan 1 Hulp Programma hebben – hoewel dat niet vaak nodig is.

In onderstaande schets probeer ik weer te geven wat het verschil is.

De bovenste (traditionele) applicatie bevat alle functies in een enkel bestand. Dus inclusief de funsies die root access nodig hebben.

De applicatie daaronder (modern) heeft de functies gescheiden. De gevaarlijk functie zitten in de Helper Tool (Hulp Programma) – welke via launchd in een meer beschermde omgeving draait.

Traditioneel vs Moderne Applicatie Design

Traditioneel vs Moderne Applicatie Design

In het nieuwe scenario, met een Hulp Programma;

Wanneer de Hoofd Applicatie start, zal alles werken zoals gebruikelijk.
Wanneer echter de functie met root access nodig is, dan wordt deze taak overgedragen aan het Hulp Programma – wat nauwlettend in de gaten wordt gehouden door launchd. De eindgebruiker merkt hier niks van, en de applicatie zou zelfs vlotter kunnen zijn dan in de traditionele methode. De Hoofd Applicatie zou op 1 processor core kunnen draaien, terwijl het Hulp Programma op een ander processor core zou kunnen draaien..

Hoofd Applicatie en Hulp Programma communiceren met behulp van berichten. Als alles goed is gedaan, dan zorgt het eerste bericht van het Hoofd Programma ervoor dat launchd automatisch het Hulp Programma start.

 Waarschuwing: Het Hulp Programma behoort een klein programma te zijn, voor een enkele taak. Het mag geen gebruikres-interface hebben en het blijkt moeite te hebben met Threads (of moeite met de implementatie van threads in Lazarus/FPC implementation). Probeer dus geen TApplication, TCustomApplication, of TThread programma te gebruiken hiervoor. Het Hulp Programma wordt dan toch door launchd geweigerd, of crasht herhaaldelijk.

 

Communicatie tussen Hoofd Applicatie en Hulp Programma

We hebben dus net geleerd dat Hoofd- en Hulp Programma communiceren m.b.v. berichten, of te wel Messages. Maar … we kunnen hier niet een willekeurig soort berichten voor gebruiken en er is een redelijke selectie van methoden.

NSXPCConnection vs CFMessagePort

De meeste Objective-C en Swift voorbeelden gebruik het XPC mechanism als IPC (Inter-Process Communicatie). Hiervoor wordt meestal de functie NSXPCConnection gebruikt. NSXPCConnection lijkt geweldig te zijn, en heeft een hoop mooie mogelijkheden. Er is echter een probleem met deze functie. Er zijn op dit moment geen zogenaamde bindings voor Lazarus Pascal gebruik, en het gebruik van specifieke Objective-C methoden maakt het waarschijnlijk erg moeilijk om dit alsnog te implementeren.

Uiteraard heb ik een alternatief gevonden op basis van een goed artikel door NSHipster, waar hij gebruik van Mach Ports via CFMessages (CFMessagePort) als suggestie vermeldt.

Nogmaals: Ik ben dus geen expert op dit gebied – suggestie zijn welkom!

Babbelen …

Om communicatie tussen Hoofd Applicatie en Hulp Programma te openen, zal eerst het Hulp Programma moeten worden geregistreerd en geïnstalleerd, en vervolgens moet het Hulp Programma een communication poort aanmaken.

In deel wordt dit gedaan m.b.v. een speciaal launchd plist bestand, welke de mach port registreert bij launchd, en vervolgens definieert welke applicatie met het Hulp Programma mag praten en onder welke condities. Deze condities hebben te maken met de CFBundleIdentifier van de Hoofd Applicatie en het feit dat het Hoofd Programma gesigned (ik kan niet eens gokken wat voor benaming men in het Nederlands gebruikt voor signen) moet zijn.

Het tweede deel van de uitdaging is dat het Hulp Programma ook werkelijk een poort open zet voor communicatie. Hiervoor gebruiken we de functie  CFMessagePortCreateLocal (meer details later).

Vanuit het perspectief van het Hoofd Programma, moeten we aangeven welke Hulp Programma’s gestart mogen worden, door het Hoofd Programma, met Elevated Privileges. Ook hier zien we soortgelijke criteria. In dit geval wordt dit gedefinieerd in het Info.plist bestand van het Hoofd Programma. En ook hier moet de applicatie gesigned worden natuurlijk.

Als dit allemaal op z’n plaats is, kunnen we met een aantal CFMessages functie gaan communiceren … als het Hulp Programma is geïnstalleerd met toestemming van de eindgebruiker (SMJobBless).

Het Hulp Programma installeren

Zoals gezegd; dit werkt dus niet voordat we het Hulp Programma hebben geïnstalleerd.
Standaard wordt dit Hulp Programma meegeleverd met het Hoofd Programma in de App Bunlde, in de directory Contents/Library/LaunchServices.

Installatie van het Hulp Programma wordt gedaan met SMJobBless, welke authentication (permissie/toestemming) nodig heeft van de eindgebruiker, welke we kunnen vragen met AuthorizationCreate.

SMJobBless vraagt vervolgens aan launchd om het Hulp Programma te installeren in /Library/PrivilegedHelperTools/.

Na installatie hoeft de eindgebruiker niet opnieuw toestemming te geven als men het Hulp Programma iwlt starten.

 SMJobBless overschrijft een eerder geïnstalleerde versie van ons Hulp Programma als je het opnieuw aanroept – Dit kan dus als mechanisme voor het Hulp Programma in /Library/PrivilegedHelperTools/ worden gebruikt. Uiteraard moet de eindgebruiker hier weer opnieuw toestemming voor geven.

Applicatie Start

Dit is de Applicatie Start Flow die ik gebruik:

Bij het staren van het Hoofd Programma, vragen we de het Hulp Programma om zijn versie nummer:
 Is het antwoord de correcte version, dan is alles goed!
Als we geen antwoord kregen  Voer dan “Installatie Hulp Programma” uit (installeer Hulp Programma)
 Als we de verkeerde versie als antwoord krijgen  Voer dan “Installatie Hulp Programma” uit (Update Hulp Programma)

De “Installatie Hulp Programma” …
 Vraag gebruiker om toestemming (Maak een Autorizatie)
Vraag launchd om het Hulp Programma te installeren (via SMJobBless)

 

Structuur en benodigdheden

Voor we aan de slag kunnen met het programmeren, eerst wat werkt dat verzet moet worden – niet alleen omdat sommige dingen vereist zijn, maar soms ook omdat het handiger is of een beter beeld geeft hoe alles werkt.

App bundle structuur

Het Hulp Programma(s) van jouw applicatie moet in de app bundle van jouw applicatie staan. Ook net zo handig voor de distributie van het programma.
Maar, zoals eerder gezegd, launchd start het Hulp Programma niet vanaf die locatie – in plaats daarvan, kopieert launchd het Hulp Programma als jouw Hoofd Programma om installatie verzoekt.

Bij een gewone Mac applicatie, zonder Hulp Programma(s), ziet de structuur van de app bundle er ongeveer zo uit (in ons voorbeeld project1.app):

Standaars App Bundle

Standaars App Bundle

De binary/executable van jouw Hoofd Applicatie staat in /Contents/MacOS  (hier “project1”).

Applicatie welke Hulp Programma(s) hebben, plaatsen het Hulp Programmas in Contents/Library/LaunchServices (hier noem ik het “com.tweaking4all.SMJobBlessHelper”) wat er zo uit ziet:

App Bundle met Helper Tool

App Bundle met Helper Tool

Installatie van het Hulp Programma

Zoals eerder gezegd, launchd start het Hulp Programma niet vanuit de app bundle.
We zullen het Hulp Programma eerst moeten “installeren” – meestal als we ons Hoofd Programma voor het eerst opstarten of na een update.

Standaard staan alle Hulp Programmas in /Library/PrivilegedHelperTools/ – een loctie waar de gebruiker zelf vaak niet bij kan omdat root access nodig is. Dus hoe krijgen we ons Hulp Programma daar?

Het antwoord is SMJobBless, een functie welke launchd vraagt om jouw Hulp Programma te installeren.
Succesvolle installatie vindt alleen maar plaats als de eindgebruiker toestemming heeft gegevn en aan allerlei criteria voldaan zijn (correct signing en de juiste plist bestanden).

Zoals eerder gezegd, we gebruiken AuthorizationCreate om toestemming te vragen van de eindgebruiker. Dit ziet er voor de eindgebruiker als volgt uit:

MacOS vraagt toestemming

MacOS vraagt toestemming

 Toestemming van de eindgebruiker is alleen nodig bij de installatie van het Hulp Programma. Het gebruik maken van een al eerder geïnstalleerd Hulp Programma vereist niet nog eens toestemming van de eindgebruiker.

Als alles goed ging, dan vindt je een kopie van het Hulp Programma in /Library/PrivilegedHelperTools/.

Je zult merken dat het Hulp Programma niet op zichzelf gestart kan worden en niet als als executable herkend wordt.

Nou dat klinkt lekker simpel he?
Nou dat is het dus niet, zeker als je, net als ik, dit allemaal met prut documentatie moet uit zien te vogelen

Een paar Lazarus Pascal tips voor we beginnen

In deze paragraaf, een paar tips die het e.e.a. makkelijk kunnen maken. Uiteraard zijn dit suggesties, dus voel je niet verplicht om het te volgen.

  1. Ik heb een Hoofd Programma project directory waarin de Hoofd Applicatie ontwikkelt wordt (hier “project1”).
  2. In deze “Hoofd Programma Project” directory, heb ik een aparte sub-directory voor het Hulp Programma project, wat een project op zichzelf is (Ik noem de directory “Helper” en het Hulp Programma “com.tweaking4all.SMJobBlessHelper”).
  3. Ik heb een .app bundle aangemaakt voor Hoofd Applicatie vanuit Lazarus, voor het testen (debugging is erg lastig!). Echter de standaard gelinkte binary wordt hier vervangen door de echte binary. Vergeet niet dat Lazarus Pascal een bundle aan maakt en de echte executable alleen maar linkt in de bundle. Dit zou hier niet werken omdat de binary gesigned miet worden en dat kan niet een gelinkte binary.
  4. Ik gebruik een klein script welke ik draai voor ik een test doe. Het script (zie hieronder) kopieert de binaries naar de juiste locatie, stript ze, en signeerd vervolgens het Hulp Programma en de .app bundle.
    
    
    1
    2
    3
    4
    5
    6
    7
    cp Helper/com.tweaking4all.SMJobBlessHelper project1.app/Contents/Library/LaunchServices/
    strip project1 project1.app/Contents/Library/LaunchServices/com.tweaking4all.SMJobBlessHelper
    codesign --force --sign "Developer ID Application: John Doe (XXXXXXXXXX)" project1.app/Contents/Library/LaunchServices/com.tweaking4all.SMJobBlessHelper

    cp project1 project1.app/Contents/MacOS/
    strip project1.app/Contents/MacOS/project1
    codesign --force --sign "Developer ID Application: John Doe (XXXXXXXXXX)" project1.app
  5. Voor het signen van jouw applicatie: (zie ook bovenstaand script), kopieer ik eerst de binaries naar de app bundle, signeer het gekopieerde Hulp Programma, en signeer vervolgens de hele app bundle.
  6. Jouw Hulp Programma project moet een “Program” zijn (“FileNewProjectProgram“) of een “Simple Program”.
    Een normale Applications (met Forms enzo), de console application, daemons, of een TCustomAppplication zullen NIET werken en worden geweigerd of crashen herhaaldelijk.
  7. Het Hoofd Applicatie project mag elk soort project zijn – zoals elk ander Lazarus Pascal project.

    Projecten aanmaken in Lazarus Pascal

    Projecten aanmaken in Lazarus Pascal

  8. Running en Debugging vanuit de Lazarus IDE is praktisch onmogelijk …
    Zowel de Hoofd Applicatie als het Hulp Programma moeten gesigneerd zijn en het Hulp Programma kan niet opzichzelf getest worden.
    Ik doe daarom altijd een “Build” voor zowel Hoofd Applicatie en Hulp Programma, draait het script, en start project1.app.
  9. In de Helper directory, heb ik een gedeelde unit met een gedeelde functie en een constante voor de Help Applicatie versie. Deze unit wordt door zowel Hoofd Applicatie als Hulp Programma gebruikt.Hierdoor zijn zowel Hoofd Applicatie en Hulp Programma netjes insync wat versie van de Hulp Applicatie betreft. Elke test start it dus met het Builden van beiden.Om er voor te zorgen dat de Hoofd Applicatie toegang heeft tot deze gedeelde unit, moeten we de directory “Helper” aan de paden van het Hoofd Programma project toevoegen (“Project “Project OptionsCompiler OptionsPathsOther Unit Files (-Fu)” voer dan “Helper” in zonder de aanhalingstekens). Vervolgens kun je de gedeelde unit toevoegen aan de uses clausule van de Hoofd Applicatie.Hier een voorbeeld zoals mijn gedeelde unit eruit ziet – je kunt het aanpassen naar wens natuurlijk.
    Merk op dat de functie CFStrToAnsiStr overgenomen is van macpgmr.github.io door Phil Hess, welke een CFStringRef naar een Pascal AnsiString converteerd. Je hebt het echt nodig! Dank je wel Phil!

    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    unit SharedFunctions;

    {$mode objfpc}{$H+}

    interface

    Uses MacOSAll;

    const HelperVersion = 'VERSION 0.0';

    function CFStrToAnsiStr(cfStr: CFStringRef; encoding: CFStringEncoding = kCFStringEncodingWindowsLatin1): AnsiString;

    implementation

    { Convert CFStringRef to a Pascal AnsiString }
    function CFStrToAnsiStr(cfStr: CFStringRef; encoding: CFStringEncoding = kCFStringEncodingWindowsLatin1): AnsiString;
    var
      StrPtr   : Pointer;
      StrRange : CFRange;
      StrSize  : CFIndex;
    begin
      if cfStr = nil then
        begin
          Result := '';
          Exit;
        end;

      {First try the optimized function}
      StrPtr := CFStringGetCStringPtr(cfStr, encoding);

      if StrPtr <> nil then  {Succeeded?}
        Result := PChar(StrPtr)
      else  {Nope - Use slower approach}
        begin
          StrRange.location := 0;
          StrRange.length := CFStringGetLength(cfStr);

          {Determine how long resulting string will be}
          CFStringGetBytes(cfStr, StrRange, encoding, Ord('?'), False, nil, 0, StrSize);
          SetLength(Result, StrSize);  {Expand string to needed length}

          if StrSize > 0 then {Convert string?}
            CFStringGetBytes(cfStr, StrRange, encoding, Ord('?'), False, @Result[1], StrSize, StrSize);
        end;
    end;

    end.

     

Apple Developer Account – Hoofd Applicatie en Hulp Programma moeten gesigneerd worden

Zowel Hoofd Applicatie als Hulp Programma moeten gesigneerd worden met een valide Apple developer certificaat, anders gaat dit echt niet werken. Je hebt dus een Apple Developer account nodig. Vroeger was dit $99/jaar, maar gelukkig is Apple wat soepeler geworden. Je kunt je inschrijven voor een gratis account (tenzij je een app in de App Store wilt zetten).

Jouw certificaat heeft een “naam”, zoiets als Developer ID Application: John Doe (XXXXXXXXXX)” (waar XXXXXXXXXX een combinatie van letters en cijfers is, en jouw Developer ID representeerd).

Maken van plists, Linken en Signeren van het Hulp Programma

Ik kan het niet genoeg zeggen, zeker niet nadat ik werkelijk weken heb lopen prullen met de soort applicatie die we nodig hebben voor een Hulp Programma: De applicatie moet van het type “program” of “simple program” zijn.

Gebruik dus NIET Console Application, applicaties gebaseerd op TApplication of TCustomApplication, Daemons/Services, en/of TThreads gebruiken. Jouw Hulp Programma zal crashen – launchd is hier best specifiek in. Uiteraard komt dit met een paar design uitdagingen, maar meer hierover later.

Per Apple SMJobBless documentatie (Engels);

In order to use this function the following requirements must be met:

 

  • The calling application and target executable tool must both be signed.
  • The calling application’s Info.plist must include a “SMPrivilegedExecutables” dictionary of strings. Each string is a textual representation of a code signing requirement used to determine whether the application owns the privileged tool once installed (i.e. in order for subsequent versions to update the installed version).

Each key of SMPrivilegedExecutables is a reverse-DNS label for the Hulp Programma (must be globally unique).

 

  • The Hulp Programma must have an embedded Info.plist containing an “SMAuthorizedClients” array of strings. Each string is a textual representation of a code signing requirement describing a client which is allowed to add and remove the tool.
  • The Hulp Programma must have an embedded launchd plist. The only required key in this plist is the Label key. When the launchd plist is extracted and written to disk, the key for ProgramArguments will be set to an array of 1 element pointing to a standard location. You cannot specify your own program arguments, so do not rely on custom command line arguments being passed to your tool. Pass any parameters via IPC.
  • The Hulp Programma must reside in the Contents/Library/LaunchServices directory inside the application bundle, and its name must be its launchd job label. So if your launchd job label is “com.apple.Mail.helper”, this must be the name of the tool in your application bundle.

Dus het Hulp Programma moet gesigneerd worden, en we moeten 2 (jawel, TWEE) plist bestanden in de binary gelinkt hebben.

De ene plist is vergelijkbaar met de standaard Info.plist van een gewone applicatie
De tweede plist is specifiek voor launchd.

In mijn project, sla ik beide plist bestand op in de Hulp Programma project directory (project1/Helper).
Deze plist worden niet meegeleverd als jij jouw applicatie gaat distribueren.

Stap 1 – Aanmaken van de Info.plist

In de eerste plist file, definieren we de BundleIdentifier (gebruik jouw eigen naam hier, hanteer reverse DNS notatie, zoiets als com.yourcompany.yourapplication) en een paar standaard properties, met als extra de property SMAuthorizedClients.

Laten we eens kijken naar een voorbeeld.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleIdentifier</key>
    <string>com.tweaking4all.SMJobBlessHelper</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>SMJobBlessHelper</string>
    <key>CFBundleVersion</key>
    <string>3</string>
    <key>SMAuthorizedClients</key>
    <array>
        <string>identifier "com.tweaking4all.SMJobBlessApp" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = XXXXXXXXXX</string>
    </array>
</dict>
</plist>

Bekende properties voor de gewone MacOS applicatie MacOS applications before, met als uitzondering dus SMAuthorizedClients.

Let goed op bij de CFBundleIdentifier van het Hulp Programma – je hebt ‘m straks nodig voor de Hoofd Applicatie.

De SMAuthorizedClients property vertelt launchd welke clienten (applicaties) met het Hulp Programma mogen “praten”. Normaal gesproken is dit jouw Hoofd Applicatie, maar het is niet verboden om een Hulp Programma te gebruiken voor meerdere programma’s.

In dit voorbeeld zien we dat alleen de gesigneerde Bundle “com.tweaking4all.SMJobBlessApp” met het Hulp Programma mag praten, en alleen als het Hoofd programma gesigneerd is met developer ID XXXXXXXXXX en wat maffe condities.

Als dit jouw eerste Hulp Programma is, kopieer dan gewoon de tekst en vervang XXXXXXXXXX met jouw developer ID en pas “com.tweaking4all.SMJobBlessApp” met de CFBundleIdentifier die je wilt gebruiken voor jouw Hoofd Applicatie.

Wat de rare conditie codes betreft: ik heb het echt niet zelf bedacht – ik heb ze overgenomen van een aantal voorbeelden.
Ik heb echter wel de moeite genomen om uit te vogelen wat ze betekenen (zie ook Code Signing Requirement Language en mijn Maffe Signing Code Details).

Wat deze codes in principe zeggen: De applicatie die met dit Hulp Programma wil communiceren, met de bundle identifier “com.tweaking4all.SMJobBlessApp” hebben, gesigneerd zijn met een legitiem Developer ID certificaat van een egschikte Certificate Authority, van het type MacOS Developer, door de ontwikkelaar met developer ID XXXXXXXXXX.

Stap 2 – Aanmaken van de Launchd plist

We hebben nog een plist nodig, en deze is specifiek voor launchd gebruik.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.tweaking4all.SMJobBlessHelper</string>
    <key>ProgramArguments</key>
    <array>
        <string>documentatie vraagt hier om, maar het doet niks</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>MachServices</key>
        <dict>
            <key>com.tweaking4all.SMJobBlessHelper</key>
            <true/>
        </dict>
    <key>StandardOutPath</key>
    <string>/var/log/t4a.stdout</string>
    <key>StandardErrorPath</key>
    <string>/var/log/t4a.stderr</string>
</dict>
</plist>

Property: Label – Dit werkt net als een CFBundleIdentifier – gebruik hier de bundle identifier van het Hulp Programma.

Property: ProgramArguments – Dit is een zinloze property voor onze toepassing, maar de documentatie zegt dat het verplicht is.

Property: RunAtLoad – Als je deze op TRUE zet, dan zal launchd de applicatie meteen starten.

Property: MachServices – Hier definieren we onze communicatie poort (ik heb de CFBundleIdentifier van het Hulp Programma gebruikt).

Optionele properties: StandardOutPath en StandardErrorPath

Deze twee zijn erg handig bij het debuggen. Bepaalde foutmeldingen en standaard output worden dan in betreffende bestanden geschreven.

Deze bestanden worden automatisch aangemaakt, dus je hoeft er niets voor te doen.
Je vindt deze bestanden ook in de Console Applicatie, in het mapje “/var/log”, mits je hetzelfde pad hebt gebruikt in de plist.

Bij distributie van jouw eindproduct zou ik ze wel verwijderen, want deze bestanden doen niets anders dan alleen maar vol lopen.

Andere Properties die je niet moet gebruiken of moet vermijden:

 Niet gebruiken: Een oude property OnDemand wordt tegenwoordig niet meer gebruikt en kun je dus gewoon weg laten.

 Vermijden: De property KeepAlive, wordt geadviseerd niet te gebruiken, en onder normale omstandig heb je het niet nodig dat een Hulp Programma eeuwig blijft draaien.

Stap 3 – Beide plists in het Hulp Programma linken

Deze was even lastig om uit te vogelen omdat ik het nog nooit nodig had – en het lijkt erop dat weinig programmeurs dit gebruiken. het ontdekken dat dit vereist was voor SMJobBless was opzich al een hele vonst.
Het vervolgens uitvoeren in Lazarus Pascal was ook weer een uitdaging op zich.

Met jouw Hulp Programma project open in Lazarus Pascal, in het menu “ProjectProject OptionsCompilation and LinkingLinking” moet je de optie  “Pass options to linker with “-k”, delimiter is spaceaanvinken en de volgende tekst invoeren;


-sectcreate __TEXT __info_plist Helper-Info.plist -sectcreate __TEXT __launchd_plist Helper-Launchd.plist

Hier is “Helper-Info.plist” jouw gewone info.plist file voor Hulp Programma, en “Helper-Launchd.plist” de launchd plist van jouw Hulp Programma launchd plist file – beiden worden verwacht in de Hulp Programma project directory (dus: /project1/Helper/).

Dit zal de plist embedden aan het einde van de binary, in de escties  __TEXT,__info_plist en __TEXT, __launchd_plist.

Je kunt dit controleren na een “Build” (waarbij Helper de binary is van jouw Hulp Programma):


$ launchctl plist __TEXT,__launchd_plist Helper
$ launchctl plist __TEXT,__info_plist Helper

Een voorbeeld van mijn project (waar XXXXXXXXXX = mijn developer ID is):


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ launchctl plist __TEXT,__launchd_plist com.tweaking4all.SMJobBlessHelper
{
    "StandardOutPath" = "/var/log/t4a.stdout";
    "StandardErrorPath" = "/var/log/t4a.stderr";
    "MachServices" = {
        "com.tweaking4all.SMJobBlessHelper" = true;
    };
    "RunAtLoad" = true;
    "Label" = "com.tweaking4all.SMJobBlessHelper";
    "ProgramArguments" = (
        "This seems required per documentation";
    );
};

$ launchctl plist __TEXT,__info_plist com.tweaking4all.SMJobBlessHelper
{
    "CFBundleIdentifier" = "com.tweaking4all.SMJobBlessHelper";
    "SMAuthorizedClients" = (
        "identifier "com.tweaking4all.SMJobBlessApp" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = XXXXXXXXXX";
    );
    "CFBundleName" = "SMJobBlessHelper";
    "CFBundleVersion" = "3";
    "CFBundleInfoDictionaryVersion" = "6.0";
};

Stap 4 – Signeren van het Hulp Programma

Signeren (dat woord klinkt zo raar!) van jouw Hulp Programma werkt net zoals het signeren van een .app bundle.
Je kon het al zien in het eerder genoemde script.

Merk op dat “HelperBinary” de gecompileerde binary is van jouw Hulp Programma, met de plist files er in gelinkt (als je de stappen correct hebt gevolgd, dan gebeurt dit automatsich).


codesign --force --sign "Developer ID Application: John Doe (XXXXXXXXXX)" HelperBinary

 

Maken van een plist Signeren van de Hoofd Applicatie

Het Hulp Programma was best lastig om uit te vogelen, met name dank aan de moeilijk te lezen en incomplete documentatie.

De Hoofd Applicatie is gelukkig een klein beetje eenvoudiger.

Stap 1 – Maken van de Info.plist

Voor de Hoofd Applicatie hebben we ook een plist nodig, net zoals met andere MacOS applicatie is ook dit geen uitzondering.

In onderstaand voorbeeld valt natuurlijk de property “SMPrivilegedExecutables” meteen op.
Deze property geeft aan welke Hulp Programmas door de applicate gestart mogen worden met elevated privileges (root/admin rechten).

Dit lijkt erg op de eerste plist van ons  Hulp Programma (waar we SMAuthorizedClients gebruikten).
Ook hier weer: Het Hulp Programma “com.tweaking4all.SMJobBlessHelper” mag gebruikt worden met Elevated Privileges, indien gesigneerd etc etc.

In tegenstelling tot het Hulp Programma, wordt deze plist in de gebruikelijk locatie in de app bundle opgeslagen (/Contents/Info.plist ).


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDevelopmentRegion</key>
  <string>English</string>
  <key>CFBundleExecutable</key>
  <string>project1</string>
  <key>CFBundleName</key>
  <string>project1</string>
  <key>CFBundleIdentifier</key>
  <string>com.tweaking4all.SMJobBlessApp</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>CFBundleSignature</key>
  <string>proj</string>
  <key>CFBundleShortVersionString</key>
  <string>0.1</string>
  <key>CFBundleVersion</key>
  <string>1</string>
  <key>SMPrivilegedExecutables</key>
  <dict>
    <key>com.tweaking4all.SMJobBlessHelper</key>
    <string>identifier &quot;com.tweaking4all.SMJobBlessHelper&quot; and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = XXXXXXXXXX</string>
  </dict>
</dict>
</plist>

Stap 2 – Signeren van de Applicatie bundle

Dit wordt gedaan net als het signeren van elke andere MacOS applicatie – let er dus wel op dat de binary (en dus niet de link naar de binary) in /Contents/MacOS moet staan. Mijn voorbeeld script regelt dit allemaal.


codesign --force --sign "Developer ID Application: John Doe (XXXXXXXXXX)" project1.app

Verificatie van Hoofd Applicatie en Hulp Programma

Na al deze stappen wil je misschien even controleren of alles goed lijkt te zijn.

Hiervoor vond ik bij Apple een Python scrip, genaamd “SMJobBlessUtil.py” (je kunt ‘m hier vinden).
Ik heb ‘m ook in mijn voorbeeld project staan die je hieronder kunt downloaden.

In de Hoofd Applicatie project directory, in Terminal, type je het volgende:


SMJobBlessUtil.py check project1.app

Als het met niks terug komt, en dus niet klaagt, dan lijkt alles goed (er kan nog vanalles fout gaan).

De meeste problemen zijn te vinden in fouten in de plist en het correct signeren van het Hulp Programma en de app bundle.

Tip: als je een fout “Error Domain=CFErrorDomainLaunchd Code=X” in de console bij een installatie poging van het Hulp Programma, controleer dan vooral de plist bestanden – je hebt daar waarschijnlijk een fout instaan.

 

Uitdaging bij het Programmeren

Nu dat we wat meer weten over de randvoorwaarden, tijd voor wat basis tips voor we gaan programmeren.

Wat we NIET kunnen in een Hulp Programma

Er zijn zeker limitaties in wat je in het Hulp Programma wel en niet kunt doen.
Ik weet dat ik in herhaling val, maar ik zeg het nog een keer:

Het volgende werkt NIET in het Hulp Programma:

  • Stanaard LCL applications, of applicaties die een gebruikersinterface hebben met elementen zoals Forms en dialogen,
  • Programa’s gebaseerd op TCustomApplication of TApplication,
  • Programa’s die TThread gebruiken, of op wat voor manier dan ook een thread open (RunCommand bijvoorbeeld),
  • De standaard Console Applicatie
  • Elke varaint van Daemon (service) applicaties

Start altijd een Hulp Programma project met het “program” of “simple program” template.

Ook al kunnen we geen widgets gebruiken, het gebruiken van de units CocoaAll en MacOSAll is OK (en nodig, zolang je maar niks met een GUI gaat doen).

Debugging Tips

Debuggen met de standaard debuggers (GDB of LLDB) is bijna onmogelijk omdat zowel Hoofd- als Hulp Programma tegelijk draaien en gesigneerd moeten zijn.

Debugger informatie is zinloos, tenzij je de hoofd applicatie aan het testen bent zonder het Hulp Programma aan te roepen.

Test of het Hulp Programma root acces heeft

Dit is werkelijk het eerste wat ik probeerde en me meteen een vroeugde dansje deed maken.
De functies FpGetUID en FpGetUID (van de BaseUnix unit) moeten beiden 0 (nul) terug geven als het programma als root draait.

In Pascal:


1
2
3
4
5
6
7
8
9
10
11
...
uses ... BaseUnix, CocoaAll ...;
...
var
  tmpString:string;
  ...
begin
  ...
  tmpString := 'UID='+IntToStr(FpGetUID)+' ,EUID='+IntToStr(FpGetUID)+' ,PID='+IntToStr(FpGetPID);
  NSLog(NSSTR('SMJOBBLESS HELPER: %@'),NSSTR(PChar(tmpString)));  
  ...

NSLog is je beste vriend: Log Messages in de Console Applicatie

Elke MacOS version komt met een aantal handige hulpmidddelen – een daarvan is de Console.
Console (deze vindt je onder “ApplicationsUtilities“) kan bijna realtime debug info geven, maar de vraag is natuurlijk; “Hoe krijg ik mijn debug meldingen in de Console?”

Hiervoor gebruiken we NSLog(), een handige functie in de CocoaAll unit.

Een paar voorbeelden hoe je dit kunt gebruiken:


1
2
3
4
5
6
NSLog(NSSTR('MYAPP - A simple log message'));  // We need to convert a Pascal string to a NSString (NS stands for Next Step, a predecessor of MacOS) in order to pass it to NSLog().
NSLog(NSSTR('MYAPP - Some Status: %d'),MyVariable); // Where "MyVariable" is an integer.
NSLog(NSSTR('MYAPP - Some Status: %ld'),MyVariable);  // Where "MyVariable" is an Int64.
NSLog(NSSTR('MYAPP - A message: %@'),NSSTR(PChar(messagePascalString)));   // Where messagePascalString is a regular Pascal string
NSLog(NSSTR('MYAPP - A messages: %@'),messageCFString);  // Where messageCFString is a NSString, NSError or CFStringRef
NSLog(NSSTR('MYAPP - Some Messages: %@, %@, %@'),messageCFString1,messageCFString2,messageCFString3);  // Where messageCFString1, 2 and 3 are a NSString, NSError or CFStringRef

NSLog() is erg flexibel en de voorbeelden zijn maar een kleine greep uit de opties die je hebt!

Om jouw meldingen sneller in Console vinden, zet ik altijd een naam voor de melding, b.v. “MIJNAPP MAIN – ” and “MIJNAPP HELPER – ” zo kan ik de Console filter gebruiken en zoen naar “MIJNAPP” om mijn berichten te zien en niet alle andere meldingen waar ik weinig mee doe.

 Opslaan van Zoekcriteria: In console, net onder de “search” box, zie je een “Save” knop alleen maar als je een zoekcriteria hebt ingevoerd en op ENTER hebt gedrukt.  Deze “Save” knop staat het toe om jouw zoekcriteria op te slaan – ze worden dan onder de knoppenbalk toegevoegd. Handig voor de volgende keer.

 Ik gebruik compiler directives om mijn debug berichten makkelijk aan en uit te zetten tijdens het compilen – zo kan ik tijdens de ontwikkelfase alle meldingen zien, maar bij het eindproduct deze meldingen uitzetten;


...
{$DEFINE DEBUG}
...
{$IFDEF DEBUG} NSLog(NSSTR('some message')); {$ENDIF}
...

Tijdens het programmeren gebruikt ik “{$DEFINE DEBUG} ” maar om dit uit te zetten maak ik er  “// {$DEFINE DEBUG}” van zodat deze meldingen niet meegenomen worden als ik het eindproduct ga compileren.

Veel Voorkomende Foutmeldingen in de Console

Er zijn er vast heel veel meer, maar dit zijn de foutmeldingen die ik regelmatig tegenkwam en waar ik wat moeite voor moest doen om te bepalen wat ze überhaupt betekenen.

Launchd Error Codes

Deze kwam ik zelf het meeste tegen toen ik net begon met SMJobBless. Omdat het wat moeite heeft gekost om dit uit te vogelen, hier een lijstje.

Je ziet ze in Console vaak in de vorm “cferrordomainlaunchd code=X” en ze worden door SMJobBless getriggert.

Launchd Error Codes
   X Error
2 kSMErrorInternalFailure
3 kSMErrorInvalidSignature
4 kSMErrorAuthorizationFailure
5 kSMErrorToolNotValid
6 kSMErrorJobNotFound
7 kSMErrorServiceUnavailable
8 kSMErrorJobPlistNotFound
9 kSMErrorJobMustBeEnabled
10 kSMErrorInvalidPlist

Een paar gevallen waar ik zelf tegen aan liep:

kSMErrorInternalFailure (2) betreft een algemene fout, vaak een probleem met een plist, of bestanden niet op de juiste plaats staan of de juiste naam hebben.
kSMErrorToolNotValid (5) geeft aan dat het Hulp Programma niet kan worden gevonden (zorg ervoor dat het in project1.app/Contents/Library/LaunchServices/ staat).

Service exited with abnormal code

Ook een hele fijne melding: “Service exited with abnormal code XYZ” (217 lijkt favoriet).

Hiervoor kon ik geen lijstje vinden, voor wat betreft wat de numerieke codes betekenen. Wel heb ik gemerkt dat het vaak een Access Violation betreft. Ik kwam ze meestal tegen als ik bezig was met TCustomApplication, Daemons, of Threads, of tijdens de allocatie van teveel geheugen (misschien dat een compiler setting dat laatste kan voorkomen).

Het is ook goed om naar de crash reports te zoeken onder “User Reports”, of in de stderr file (mits je een bestand hebt opgegeven in launchd plist).

Gebruik de launchd plist’s “StandardOutPath” en “StandardErrorPath” properties

Zoals eerder vermeld, gebruik de StandardErrorPath en StandardErrorPath in de launchd plist file van het Hulp Programma.

Ik gebruik ze minder vaak dan NSLog() maar ze hebben me toch een paar keer goed geholpen.
Let wel op dat je betreffende bestanden wel zelf moet opruime.

Je kunt ze verwijderen met sudo rm /var/log/<yourlogfile>. Als je het Hulp Programma weer start, dan worden ze vanzelf weer aangemaakt.

Crash Reports

De Console applicatie logs crash reports onder de section “User Reports”.
Ik vond deze minder behulpzaam omdat er een berg info in staat waar ik de helft van de tijd geen idee van heb wat ik er mee moet.
Maar … er zijn situaties geweest waarbij het Hulp Programma crashte en er niks in de stderr file stond, maar wel in de crash reports.

Stoppen van een eigenwijs Hulp Programma

Ik heb het een paar keer meegemaakt dat mijn Hulp Programma gewoon NIET wilde stoppen, zelfs niet na het killen van de task – het blijf gewoon opnieuw starten. Het weghalen van het Hulp Programma uit /Library/PrivilegedHelperTools werkt vaak ook niet, omdat het Hulp Programma in het geheugen zit. je moet dus beiden doen!

Dit is overigens niet perse de juiste manier, maar in geval van nood werkt het wel:


1
2
3
4
5
$ sudo rm /Library/PrivilegedHelperTools/com.tweaking4all.SMJobBlessHelper
$ ps -ax | grep tweaking
35617 ??         0:00.03 /Library/PrivilegedHelperTools/com.tweaking4all.SMJobBlessHelper
35677 ttys000    0:00.00 grep tweaking
$ sudo kill -9 35617

Het PID komt van de regel waar het volledige pad van jouw Hulp Programma in staat.

Controleer jouw launchd plist, en kijk of je niet per ongeluk de property “KeepAlive” gebruikt (Apple adviseert deze te vermijden).

Launchd blijft jouw Hulp Programma herstarten als je geen elegante “exit” hebt ingebouwd.

Er zijn vast meer tucks, post jouw suggestie hieronder!

Zie of jouw Hulp Programma actief is

Er zijn een aantal methoden om te zien of jouw Hulp Programma actief is – Let we dat launchd jouw Hulp Programma start wanneer het nodig is, en dat kan nog niet gebeurt zijn, en wil dus niet zeggen dat er iets mis is.

Als eerste kun je kijken of jouw Hulp Programma in de /Library/PrivilegedHelperTools/ directory staat. Mocht die er niet staan dan is er iet mis gegaan bij installatie. Merk ook op dat je jouw Hulp Programma NIET met de hand kunt starten!

Je kunt kijken of jouw Hulp Programma met launchctl, hier een voorbeeld voor mijn eigen Hulp Programma:


1
2
3
$ launchctl list | grep com.tweaking4all

25101   0   com.tweaking4all.SMJobBlessApp.34924

Mocht jouw Hulp Programma er niet tussen staan, dan wil dit niet zeggen dat er iets fout is gegaan. Launchd heeft het gewoon nog niet nodig gehad.

Merk op; Het eerste nummer is de PID (process ID) en de tweede de exit code.
In het algemeen; exit code = 0 , betekent alles werkt(e) OK.
Een positieve exit code geeft vaak aan dat er een error was, en een negatief nummer wil zeggen dat de taak gestopt is op basis van een signaal.

Als alternatief kun je “ps -ax | grep <yourhelpertoolbundleidentifier> ” gebruiken.
Het is interessant om te zien dat de PID in launchctl anders is dan de PID met “ps”.

Ik vermoed dat de PID van launchctl de Mach port aangeeft, en die van “ps” de executable.

 

Programmeren van de Hoofd- en Hulp Programma

Nou dat was een hoop lees werk, tijd om aan het Hoofd- en Hulp Programma te gaan beginnen.

Theoretisch zou je eerst het Hulp Programma kunnen maken en daarna het Hoofd Programma – maar in dit project, waarschijnlijk ook omdat het de eerste keer was, bleek het toch prettiger om beiden tegelijk te doen. Of op z’n minst (in mijn geval) een test Hoofd Programma en het Hulp Programma tegelijk doen. Dus een dummy programma om het Hulp Programma te testen.

Omdat ik er ook even mee zat te klooien, hier een ruwe schets hoe het e.e.a. aangeroepen wordt, om je een idee te geven:

Berichten Flow

Berichten Flow

 

Communicatie met Messages

Je weet al dat we beide programma’s via berichten met elkaar communiceren – denk daar eerst goed over na en bedenk een soort van “protocol” waarmee Hoofd Applicatie en Privileged Hulp Programma gaan communiceren.

Ik zal mijn voorbeeld baseren op een Hulp Programma dat een backup gaat maken waar nodig.

Dit voorbeeld is geheel gebaseerd op CFMessages – omdat ik niks kon beginnen met NSXPCConnection. Mocht je NSXPCConnection aan de gang krijgen, laat het me dan vooral weten.

Defineer een Communicatie Protocol

Communication werk in principe maar in 1 richting: Van Hoofd Applicatie naar Hulp Programma.
Het Hulp Programma kan beperkt een vlotte reactie terug sturen, maar dat kan maar 1 enkele keer per bericht van dat van Hoofd Applicatie naar Hulp Programma wordt gestuurd.

Een voorbeeld protocol:

Communication “protocol”
Naar Hulp Programma Reactie van Hulp Programma  Actie
 GETVERSION  VERSION x.y  Stuur versie nummer van Hulp Programma
 DOSOMETHING  OK  Start een taak met root access en retour “OK” als het gestart is
 STATUS  <een nummer>  Vraag om de status van een een taak (-1 indien idle)
 ACTIVE  YES of NO  Vraag of het Hulp Programma bezig is met een taak
 LASTERROR  Some sort of error  Geef de laatste foutmelding van het Hulp Programma
 QUIT  OK  Stuurt “OK” retour en sluit het Hulp Programma

Bedenk dus goed dat dit een erg simpel protocol is. Je kunt er ook niet al te veel van verwachten. Alle luxe van standaard Lazarus Pascal applicaties, zoals event triggers, hebben we met berichten en een “plat” Hulp Programma, niet ter beschikking.

Het eerste bericht dat naar een Hulp Programma wordt gestuurd, zorgt ervoor dat launchd het Hul Programma start. Dit bericht gaat gelukkig niet verloren, en als het Hulp Programma gestart is, ontvangt het meteen dat eerste bericht.

Ik heb in mijn Hulp Porgramma ook een optie ingebakken om het Hulp Programma af te sluiten – het is een verkwisting van resources als ons Hulp Programma in de achtergrond blijft lopen, terwijl we er niets mee doen (vandaar het “QUIT” bericht).

In dit voorbeeld, start ik het Hoofd Programma en stuur ik het Hulp Programma meteen het bericht “GETVERSION” zodat ik weet welke versie van het Hulp Programma geïnstalleerd staat op basis van het retour bericht “VERSION x.y”. Indien het de verkeerde versie is, of in het geval we geen reactie krijgen, dan kunnen we het Hulp Programma alsnog installeren.

Het is dus wel belangrijk dat je er voor zorgt dat jouw Hulp Programma prompt een antwoord retour kan geven, anders volgt er een time-out en krijgt het Hoofd Applicatie geen retour antwoord.

Hulp Programma – Mach Port voor ontvangen van CFMessages

Dit is een beetje het kip en het ei, dus laten we beginnen met het Hulp Programma. Zorg ervoor dat de je de correct plists hebt, en dat beide programma’s gesigneerd zijn (in de launch plist registreren we de communcatie “poort”, zie “Stap 2 – Creating the Launchd plist“).

Het aanmaken en openen van een communicatie poort gaat relatief eenvoudig met CFMessagePortCreateLocal, en ziet er ongeveer zo uit:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
program com.tweaking4all.SMJobBlessHelper;
{$linkframework ServiceManagement}
{$linkframework Security}
{$linkframework Foundation}
{$linkframework CoreFoundation}
{$calling mwpascal}

// Comment out this line to stop showing debug messages in the console:
{$DEFINE DEBUG}

uses Classes, SysUtils {$IFDEF DEBUG}, CocoaAll {$ENDIF}, MacOSAll, BaseUnix;

function ReceiveMessage(local:CFMessagePortRef; msgid: SInt32; data: CFDataRef; info:Pointer): CFDataRef; mwpascal;
begin
  ...
end;

// ***** MAIN *****
var
  tmpString:string;
  receivingPort:CFMessagePortRef;
  context: CFMessagePortContext;
  shouldFreeInfo:boolean;
begin
  {$IFDEF DEBUG}
  tmpString := 'Started at '+TimeToStr(Now)+': UID='+IntToStr(FpGetUID)+' ,EUID='+IntToStr(FpGetEUID)+' ,PID='+IntToStr(FpGetPID);
  NSLog(NSSTR('SMJOBBLESS HELPER: %@'),NSSTR(PChar(tmpString)));
  {$ENDIF}

  receivingPort := CFMessagePortCreateLocal(nil, CFSTR('com.tweaking4all.SMJobBlessHelper'), @ReceiveMessage, context, shouldFreeInfo);

  {$IFDEF DEBUG}
  if receivingPort=nil then
    NSLog(NSSTR('SMJOBBLESS HELPER: FAILED CFMessagePortCreateLocal'))
  else
    NSLog(NSSTR('SMJOBBLESS HELPER: SUCCESS CFMessagePortCreateLocal'));
  {$ENDIF}

  ...

  CFRelease(receivingPort);
  ExitCode:=0;
end.

Zoals je ziet; waarschijnlijk de meest eenvoudige vorm van een Pascal program -. doet me denken aan de Turbo Pascal dagen.

Je ziet dat ik een aantal frameworks (ik had ze misschien niet allemaal nodig) gelinkt, en de calling als “mwpascal” gedefinieerd zodat ons Pascal programma correct op en neer babbelt met de MacOS framework API calls.

Je ziet ook een lege “ReceiveMessage” functie. Deze gaan we straks gebruiken als “CallBack” function.

En natuurlijk een hoop code die ons gaat helpen bij het DEBUG proces. Ik kan dit sterk aanranden zodat we plenty zien in Console en mogelijke problemen sneller kunnen vinden.

De functie CFMessagePortCreateLocal (onderdeel van Core Foundation) is als volgt gedefinieerd:


1
2
3
4
5
// Obejctive-C
CFMessagePortRef CFMessagePortCreateLocal(CFAllocatorRef allocator, CFStringRef name, CFMessagePortCallBack callout, CFMessagePortContext *context, Boolean *shouldFreeInfo);

// Pascal - unit CFMessagePort
function CFMessagePortCreateLocal( allocator: CFAllocatorRef; name: CFStringRef; callout: CFMessagePortCallBack; var context: CFMessagePortContext; var shouldFreeInfo: Boolean ): CFMessagePortRef; external name '_CFMessagePortCreateLocal';

De “allocator” (een CFAllocatorRef) wordt gebruikt voor het alloceren van geheugen voor het nieuwe communicatie object.
We geven hier de waarde “nil” door, zodat de standaard allocator (kCFAllocatorDefault) wordt gebruikt.

De “name” is de naam die we willen gaan gebruiken voor de poort. ik heb er hier voor gekozen om de bundle identifier van het Hulp Programma te gebruiken. Let wel op dat die string well een CFStringRef (pointer naar een CFString) moet zijn, dus we hebben de functie CFSTR (unit MacOSAll) nodig om een gewone Pascal String te converteren.

callout” is het adres van de functie, die het externe framewerk (in ons programma) gaat aanroepen als er een bericht aankomt.

Deze functie geeft “nil” terug als er iets fout ging!

De callback functie moet het volgende formaat volgen:


1
2
3
4
5
// Objective-C
typedef CFDataRef (*CFMessagePortCallBack)(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void *info);

// Pascal - unit CFMessagePort
CFMessagePortCallBack = function( local: CFMessagePortRef; msgid: SInt32; data: CFDataRef; info: UnivPtr ): CFDataRef;

Dus in Pascal doen we dat bijvoorbeeld zo:


function ReceiveMessage(local:CFMessagePortRef; msgid: SInt32; data: CFDataRef; info:Pointer): CFDataRef; mwpascal;

 

De parameter “context” geeft contextual informatie over de messageport terug, dus dit moet een variable van het type CFMessagePortContext zijn. Ik had er (nog) geen gebruik voor.

Als laatste parameter hebben we “shouldFreeInfo” wat een boolean terug geeft, en je vertelt of jij (TRUE) context vrij moet geven of niet (FALSE). Dit gaat dus door een boolean variable.

OK we hebben een poort gemaakt, maar ja, nu wat? We kunnen het niet eens testen.
Nog erger, zonder een Hoofd Applicatie, kunnen we het Hulp Programma niet eens installeren – kip en het ie gevoel?

Hoofd Applicatie – Installeer het Hulp Programma

Mijn Hoofd Applicatie is een standaard Lazarus Pascal Application, met Forms, knoppen etc.
Ik heb daar voor elke stap een knop gemaakt zodat ik wat kan testen en spelen (zie downloadbaar voorbeeld).

Om te kunnen babbelen met onze nagel nieuwe communicatie poort in het Hulp Programma, moeten we dus eerst launchd zo gek krijgen dat het ons Hulp Programma gaat installeren.

Stap 1 – Hoofd Applicatie – Open Communicatie met het Hulp Programma

In het voorbeeld programma zie je dat ik een “Initialize” knop heb gezet, die eerst gaat proberen communicatie op te zetten met ons Hulp Programma om vervolgens om een versie nummer te vragen.

Uiteraard gaat dit falen, want er is nog geen Hulp Programma geïnstalleerd, maar we zien nu tenminste hoe we communicatie gaan opzetten.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
const
  helperLabel = 'com.tweaking4all.SMJobBlessHelper';
...
var
  messageID: SInt32;
  timeout:CFTimeInterval;
  HelperPort:CFMessagePortRef;
  errorString:string;
  answerString:CFStringRef;
begin
  ...
  { Create a communication port }
  HelperPort := CFMessagePortCreateRemote(nil, CFSTR(helperLabel));

  { Check if port opened - if not; try to re-install the Hulp Programma }
  if (HelperPort=nil) and not(installHelperTool) then
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS APP: Failed to Install and/or Connect to Hulp Programma')); {$ENDIF}
      Result:=false;
      exit;
    end;
  ...
end;

We CFMessagePortCreateRemote om een poort te openen, en dit is als volgt gedefinieert:


1
2
3
4
5
// Objective-C
CFMessagePortRef CFMessagePortCreateRemote(CFAllocatorRef allocator, CFStringRef name);

// Pascal - unit CFMessagePort
function CFMessagePortCreateRemote( allocator: CFAllocatorRef; name: CFStringRef ): CFMessagePortRef; external name '_CFMessagePortCreateRemote';

Net als bij het Hulp Programma, hebben weer een allocator nodig (we gebruiken weer de waarde nil zodat de standaard allocator, kCFAllocatorDefault, gebruikt gaat worden), en een name van de poort. We gebruiken als naar de naam die we in het Hulp Programma hebben gedefinieerd.

Dit gaat natuurlijk nog niks voor ons doen, we moeten nog een bericht versturen – daar komen we straks op terug.
We weten nu, in de eerste ronde, al dat het fout gaat – Het Hulp Programma bestaat nog niet, dus we krijgen die poort ook niet open, en we weten al dat we door kunnen naar stap 2: de installatie van het Hulp Programma.

 Als deze functie “nil” retour geeft, dan is er dus iets fout gegaan.

Stap 2 – Installeer het Hulp Programma

In deze stap, gaan we launchd vragen om het Hulp Programma te installeren. Hiervoor gaan we SMJobBless gebruiken.

Installatie van het Hulp Programma vereist correct plist bestanden, gelinkt in het Hulp Programma, en zowel Hoofd Programma als Hulp Programma moet gesigneerd zijn, anders werkt dit dus gegarandeerd NIET!


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const
  kSMRightBlessPrivilegedHelper = 'com.apple.ServiceManagement.blesshelper';
  helperLabel = 'com.tweaking4all.SMJobBlessHelper';
...
function SMJobBless(domain:CFStringRef; executableLabel:CFStringRef; auth:AuthorizationRef; outError:CFErrorRef): boolean; external name '_SMJobBless'; mwpascal;
var kSMDomainSystemLaunchd: CFStringRef; external name '_kSMDomainSystemLaunchd';
...
function TForm1.installHelperTool:boolean;
var
  status:OSStatus;
  authItem: AuthorizationItem;
  authRights: AuthorizationRights;
  authFlags: AuthorizationFlags;
  authRef: AuthorizationRef;
  error:NSError;
  AuthenticationCount:integer;
begin
  Result := False;

  { Get proper Authentication }

  authItem.flags := 0;
  authItem.name  := kSMRightBlessPrivilegedHelper;
  authItem.value := nil;
  authItem.valueLength:= 0;

  authRights.count := 1;
  authRights.items := @authItem;

  authFlags := kAuthorizationFlagDefaults or kAuthorizationFlagInteractionAllowed or kAuthorizationFlagPreAuthorize or kAuthorizationFlagExtendRights;

  authRef := nil;
  error   := nil;

  AuthenticationCount:=0;

  { Get authentication to install helper }

  status := AuthorizationCreate(@authRights, kAuthorizationEmptyEnvironment, authFlags, authRef);

  if status<>errAuthorizationSuccess then
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS APP: Authentication Failed (Error %d)'),AuthenticationCount, status); {$ENDIF}
      Result := false;
      Exit;
    end;

  { Attempt Hulp Programma install if authentication succeeds }

  if status=errAuthorizationSuccess then
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS APP: Trying to install Hulp Programma')); {$ENDIF}

      { SMJobBless - Try to actually installl privileged Hulp Programma - overwrites any existing version automatically }
      Result := SMJobBless(kSMDomainSystemLaunchd,CFSTR(helperLabel),authRef,@error);
     
      if Result then
        begin
          {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS APP: Hulp Programma Installed')); {$ENDIF}
        end
      else
        begin
          {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS APP: Helper Install Failed (Error: %@)'),error); {$ENDIF}
        end;
    end
  else
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS APP: Authentication Failed (Status: %d)'),status); {$ENDIF}
    end;

  AuthorizationFree(authRef,0);
end;

Er gebeurt hier nogal wat en ik zal proberen het e.e.a. toe te lichten.

Stap 2A – Toestemming Vragen

Voor we ons Hulp Programma met Elevated Privileges (root access) kunnen installeren, moeten we toestemming krijgen van de eindgebruiker.
We doen dit met AuthorizationCreate, welke een berg parameters gebruikt om onder andere te definiëren wat ons Hulp Programma wel of niet zou mogen als het eenmaal geïnstalleerd is.

De AuthorizationCreate is als volgt gedefinieert en geeft een status terug:


1
2
3
4
5
// Objective-C
OSStatus AuthorizationCreate(const AuthorizationRights *rights, const AuthorizationEnvironment *environment, AuthorizationFlags flags, AuthorizationRef  _Nullable *authorization);

// Pascal - unit Authorization
function AuthorizationCreate( rights: AuthorizationRightsPtr; environment: AuthorizationEnvironmentPtr; flags: AuthorizationFlags; var authorization: AuthorizationRef ): OSStatus; external name '_AuthorizationCreate';

De parameter “rights” definieert welke rechten we ons Hulp Programma willen geven als het straks draait. We kunnen de waarde nil doorgegeven als we geen rechten nodig hebben, maar in ons voorbeeld hebben we dat echt wel nodig. Omdat we root access zoeken, hebben we “kSMRightBlessPrivilegedHelper” nodig. Deze rechten slaan we op in een array (authRights) van rechten. In ons voorbeeld hebben maar een enkel rechten-item (authItem) nodig, welke we in deze array moeten plaatsen.

Omdat de huidige bindings van Lazarus Pascal kSMRightBlessPrivilegedHelper nog niet definieert, zullen we dit zelf moeten doen met:


const  kSMRightBlessPrivilegedHelper = 'com.apple.ServiceManagement.blesshelper';

De volgende parameter is “environment” welke we op “kAuthorizationEmptyEnvironment” zetten, wat in principe wil zeggen dat we beginnen met een lege autorisatie omgeving beginnen. Met “environment” kunnen dingen zoals een icon of een tekst voor de prompt, en zelfs een gebruikersnaam en wachtwoord doorgeven die dan in de toestemmings dialoog gebruikt kan worden (zie AuthorizationItemSet).

Opmerkingen over de “environment” parameter

  • Deze wordt niet gebruikt in OS X v10.2 of ouder,
  • In macOS 10.3 en nieuwer, kun je een icon en een tekst doorgevn voor de authentication dialog box.
  • In macOS 10.4 en nieuwer, kun je ook nog eens gebruikersnaam en wachtwoord meegeven voor autorisatie zonder dat de eindgebruiker hier iets van gaat zien (zie AuthorizationTags.h).

De AuthorizationItemSet is ook weer een array met items, elke item met een “value” en een “valueLenght”. Indien een tekst wordt meegegeven dan wordt deze in “value” opgeslagen en “valueLenght” is dan een reflectie van de data lengte van “value”.

AuthorizationTags.h
Key Defined as Value
kAuthorizationEnvironmentUsername “username” De gebruikersnaam (10.4+)
kAuthorizationEnvironmentPassword “password” Het wachtwoord van de gebruiker (10.4+)
kAuthorizationEnvironmentShared “shared” (Niet van toepassing)
kAuthorizationRightExecute “system.privilege.admin” Volledig pad van tool dat je wilt uitvoeren
kAuthorizationEnvironmentPrompt “prompt” Tekst voor de toestemming dialoog
kAuthorizationEnvironmentIcon “icon” Volledig pad naar een afbeelding geschikt voor NSImage

Ik heb er nog geen gebruik van gemaakt, maar dat ga ik zeker binnenkort doen!

 

De parameter “flags” staat ons toe wat autorisatie opties te definiëren en we combineren ze met het OR statement.

AuthorizationFlags
 Flag Toepassing
kAuthorizationFlagDefaults Begin punt – doet niks en is een zogenaamde place holder
kAuthorizationFlagInteractionAllowed Gebruikersinteractie toegestaan indien nodig
kAuthorizationFlagExtendRights Security Server poging tot toekennen van gevraagde rechten
kAuthorizationFlagPartialRights Security Server poging tot toekennen van rechten op individuele basis
kAuthorizationFlagDestroyRights Vertel de Security Server om autorisatie in te trekken
kAuthorizationFlagPreAuthorize Vertel de Security Server om een voor-autorisatie m.b.t. gevraagde rechten
kAuthorizationFlagNoData Prive flag, niet gebruiken!

Zoals je in de code ziet heb ik kAuthorizationFlagDefaults, kAuthorizationFlagInteractionAllowed, kAuthorizationFlagPreAuthorize en kAuthorizationFlagExtendRights gebruikt en eerlijk gezegd is dat gebaseerd op voorbeelden elders.

De laatste parameter, “authorization“, autorisatie referentie die we voor SMJobBless nodig hebben.

 Alleen als AuthorizationCreate de waarde “errAuthorizationSuccess” terug geeft, is autorisatie gelukt (en heeft de eindgebruiker dus toestemming gegeven) — in alle ander gevallen ging dit dus fout!

Stap 2B – Installeer Hulp Programma met SMJobBless

Nu we autorisatie hebben, kunnen we eindelijk SMJobBless gebruiken en ons Hulp Programma laten installeren door launchd.

Ook hier weer helaas, maar de bindings van Lazarus Pascal bevatten “SMJobBless” nog niet, dus ook dat moest ik handmatig toevoegen (het is een functie van het Service Management framework). Dit geldt ook voor de variabele “kSMDomainSystemLaunchd“.

SmJobBless is als volgt gedefinieerd:


1
2
3
4
5
6
7
8
9
10
// Objective-C
Boolean SMJobBless(CFStringRef domain, CFStringRef executableLabel, AuthorizationRef auth, CFErrorRef *outError);

const CFStringRef kSMDomainSystemLaunchd;


// en in Pascal wordt dat:
function SMJobBless(domain:CFStringRef; executableLabel:CFStringRef; auth:AuthorizationRef; outError:CFErrorRef): boolean; external name '_SMJobBless'; mwpascal;

var kSMDomainSystemLaunchd: CFStringRef; external name '_kSMDomainSystemLaunchd';

De functie geeft TRUE terug als installatie succesvol was. Is het antwoord FALSE dan is het zaak even naar de waarde van “outError” te kijken – NSLog kan dit meteen weergeven zonder conversie.

De “domain” parameter, is op het moment een maffe omdat allen de waarde kSMDomainSystemLaunchd wordt ondersteund.

De parameter “executableLabel” moet overeenkomen met label property in de launchd plist van het Hulp Programma dat we willen installeren. Deze label moet ook in de Info.plist van het Hoofd Programma staan, onder de property SMPrivilegedExecutables.

De autorisatie referentie “auth” is de autorisatie die we net met AuthorizationCreate hebben aangemaakt.

outError” is een return value voor de foutmelding. mocht zich een fout voordoen – NSLog geeft dat direct weer als leesbare tekst.

Vragen om het Correctie Versie Nummer van het Hulp Programma

Hoofd Applicatie – Bericht Versturen en Response Ontvangen

Nu dat we het Hulp Programma kunnen installeren, en geschikt hebben gemaakt voor het ontvangen van een bericht, tijd om een bericht te gaan versturen.

De stappen zijn eenvoudig:

  1. Open een communicatie poort
  2. Verstuur een bericht (verzoek)

Ik heb dit allemaal in een eenvoudige functie geplakt waarin je het openen van de poort ook weer terug vindt;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function TForm1.SendMessage(aMessage:string):string;
var
  data, returndata:CFDataRef;
  messageString:string;
  messageID: SInt32;
  status:SInt32;
  timeout:CFTimeInterval;
  HelperPort:CFMessagePortRef;
  answerCFString:CFStringRef;
begin
  timeout   := 1; // 2 second timeout
  messageID := 0; // arbitrary
  Result:='';

  { Create a communication port }
  HelperPort := CFMessagePortCreateRemote(nil, CFSTR(helperLabel));

  { Check if port opened - if not; try to re-install the Hulp Programma }
  if (HelperPort=nil) then
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS APP: Failed to Connect to Hulp Programma')); {$ENDIF}
      Result:='';
      exit;
    end;

  { Verify Hulp Programma version - in case of needed update }
  {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS APP: Sending Message = %@'),CFSTR(PChar(aMessage))); {$ENDIF}

  messageString := aMessage;
  data := CFStringCreateExternalRepresentation(nil,CFSTR(PChar(messageString)),kCFStringEncodingUTF8,0);

  status := CFMessagePortSendRequest(HelperPort,
                                     messageID,
                                     data,
                                     timeout,
                                     timeout,
                                     kCFRunLoopDefaultMode,
                                     @returndata);

  { Message sent? - Yes: Check version and reinstall if needed }
  if status = kCFMessagePortSuccess then
    begin
      if returndata=nil then
        {$IFDEF DEBUG}
        NSLog(NSSTR('SMJOBBLESS APP: Reply Received (SendMessage - empty)'))
        {$ELSE}
        //
        {$ENDIF}
      else if returndata<>nil then
        begin
          { Copy Answer }
          answerCFString := CFStringCreateFromExternalRepresentation(nil,returndata,kCFStringEncodingUTF8);
          Result         := CFStrToAnsiStr(answerCFString);

          {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS APP: Reply received: %@'),answerCFString); {$ENDIF}
          CFrelease(answerCFString);
        end;
    end;

  if HelperPort<>nil then CFRelease(HelperPort);
end;

We hebben 2 functies nodig voor het versturen van een bericht: CFMessagePortSendRequest en CFStringCreateExternalRepresentation.
Die laatste is niet zo belangrijk in het proces. Het maakt alleen een lokale kopie van bepaalde data, voor het geval de originele externe data ineens verdwijnt.

CFMessagePortSendRequest is als volgt gedefinieerd:


1
2
3
4
5
// Objective-C
SInt32 CFMessagePortSendRequest(CFMessagePortRef remote, SInt32 msgid, CFDataRef data, CFTimeInterval sendTimeout, CFTimeInterval rcvTimeout, CFStringRef replyMode, CFDataRef *returnData);

// Pascal - Unit CFMessagePort
function CFMessagePortSendRequest( remote: CFMessagePortRef; msgid: SInt32; data: CFDataRef; sendTimeout: CFTimeInterval; rcvTimeout: CFTimeInterval; replyMode: CFStringRef; returnData: CFDataRefPtr ): SInt32; external name '_CFMessagePortSendRequest';

En hier weer een lijstje parameters …. vergeet niet dat het verzenden van een bericht een “verzoek” is – let dus op de return waarde van de functie.

De parameter “remote” is de poort welke we met CFMessagePortCreateRemote hebben geopend.

msgid” is een willekeurig nummer – voor het geval je jouw berichten wilt tracken.

De “data” betreft het uiteindelijke bericht dat we willen versturen.

Met de “sendTimeout” parameter, kunnen we aangeven hoe lang we willen wachten tot het “verzoek” is ingewilligd (= bericht verzonden).

De “rcvTimeout” parameter geeft aan hoe lang we willen wachten op een antwoord.

De parameter “replyMode” bepaalt hoe we om willen gaan met het reply versturen in het Hulp Programma.

Uit eigen ervaring: Gebruik de kCFRunLoopDefaultMode constante tenzij je goed weet waarom je een andere ReplyMode zou willen.

En dan natuurlijk de “returnData” variable die de response van het Hulp Programma bevat (als er een response was).

De return waarde van de functie (SInt32) is een van deze waarden:

 

CFMessagePortSendRequest Error Codes
Value Error
kCFMessagePortSuccess Bericht verstuurd, en indien gevraagd, een antwoord ontvangen.
kCFMessagePortSendTimeout Bericht versturen faalde (timeout)
kCFMessagePortReceiveTimeout Bericht verstuurd, maar antwoord faalde (timeout)
kCFMessagePortIsInvalid Bericht versturen faalde – ongeldige message port
kCFMessagePortTransportError Bericht versturen faalde – er is een foutmelding
kCFMessagePortBecameInvalidError Bericht versturen faalde – message poort werd ongeldig verklaart

Omdat ik dit in een (simpele) functie heb gegoten, kunnen we nu eenvoudig een bericht versturen en zoiets als dit doen:


if Sendmessage('GETVERSION')<>'VERSION 1.0" then ...

Je kunt dit waarschijnlijk al testen, ook al doet het nog niks zinvols.

Hulp Programma – Bericht Ontvangen en Antwoord Retour

Om een fatsoenlijk antwoord terug te sturen, moeten we natuurlijk de inkomende berichten ergens opvangen en verwerken. Dit gaat via de eerder gemaakt (lege) callback functie RecieveMessage().

Met het communicatie protocol in gedachten, hier een simpel voorbeeld:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function ReceiveMessage(local:CFMessagePortRef; msgid: SInt32; data: CFDataRef; info:Pointer): CFDataRef; mwpascal;
var
  messageString:CFStringRef;
  messagePascalStr:string;
  returnString:string;
  returnData:CFDataRef;
begin
  messageString:=CFStringCreateFromExternalRepresentation(nil,data,kCFStringEncodingUTF8);
  {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS HELPER: Received Message = %@'),messageString); {$ENDIF}
  messagePascalStr:=CFStrToAnsiStr(messageString);

  if messagePascalStr='GETVERSION' then
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS HELPER: CMD - GET VERSION')); {$ENDIF}
      returnString := 'VERSION 1.0';
    end
  else if messagePascalStr='ACTIVE' then
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS HELPER: CMD - CHECK IF ACTIVE')); {$ENDIF}
      returnString := 'NO';
    end
  else if messagePascalStr='DOSOMETHING' then  
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS HELPER: CMD - DO SOMETHING')); {$ENDIF}
      returnString := 'Something complicated just Started';
    end
  else if messagePascalStr='LASTERROR' then
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS HELPER: CMD - GET LAST ERROR')); {$ENDIF}
      returnString := 'We had no errors - all good!';
    end
  else if messagePascalStr='QUIT' then
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS HELPER: CMD - QUIT HELPER')); {$ENDIF}
      returnString := 'OK';
      //TerminateHelper:=true;
    end
  else
    begin
      {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS HELPER: CMD - UNKNOWN')); {$ENDIF}
      returnString := 'UNKNOWN COMMAND';
    end;

  returndata := CFStringCreateExternalRepresentation(nil,CFSTR(PChar(returnString)),kCFStringEncodingUTF8,0);
  Result := returnData;
end;

Lekker simpel toch? Als je naar het downloadbare project kijkt, dan zie je wat voorbeelden om mee te gaan spelen.

Hulp Programma – In Rondjes Rennen voor Berichten

Voor dit stukje van de code moest ik even m’n grijze cellen resetten naar de 90’er jaren – toen ik nog rommelde met Turbo Pascal – en je zult zo zien waarom.

Niet Vastlopen in een Loop

Om berichten te kunnen blijven ontvangen in het Hulp Programma, hebben we een soort loop nodig. De luxe die we hadden in standaard Lazarus Pascal applictcaties, zoals events etc, is helaas niet beschikbaar in een simpel programma waarin we geen TApplication en threads mogen gebruiken.

Hieronder een voorbeeld van de loop, in het Hulp Programma, voor het ontvangen van berichten;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var
  tmpString:string;
  receivingPort:CFMessagePortRef;
  receivingRunLoop:CFRunLoopSourceRef;
  context: CFMessagePortContext;
  shouldFreeInfo:boolean;
  reason:SInt32;
begin
  {$IFDEF DEBUG}
  tmpString := 'Started at '+TimeToStr(Now)+': UID='+IntToStr(FpGetUID)+' ,EUID='+IntToStr(FpGetEUID)+' ,PID='+IntToStr(FpGetPID);
  NSLog(NSSTR('SMJOBBLESS HELPER: %@'),NSSTR(PChar(tmpString)));
  {$ENDIF}

  TerminateHelper := false;

  receivingPort:=CFMessagePortCreateLocal(nil,CFSTR('com.tweaking4all.SMJobBlessHelper'),@ReceiveMessage,context,shouldFreeInfo);

  {$IFDEF DEBUG}
  if receivingPort=nil then
    NSLog(NSSTR('SMJOBBLESS HELPER: FAILED CFMessagePortCreateLocal'))
  else
    NSLog(NSSTR('SMJOBBLESS HELPER: SUCCESS CFMessagePortCreateLocal'));
  {$ENDIF}

  receivingRunLoop:= CFMessagePortCreateRunLoopSource(nil, receivingPort, 0);

  {$IFDEF DEBUG}
  if receivingRunLoop=nil then
    NSLog(NSSTR('SMJOBBLESS HELPER: FAILED CFMessagePortCreateRunLoopSource'))
  else
    NSLog(NSSTR('SMJOBBLESS HELPER: SUCCESS CFMessagePortCreateRunLoopSource'));

  NSLog(NSSTR('SMJOBBLESS HELPER: Executing CFRunLoopAddSource'));
  {$ENDIF}

  CFRunLoopAddSource(CFRunLoopGetCurrent(), receivingRunLoop, kCFRunLoopDefaultMode{kCFRunLoopCommonModes});

  //if CloningProcessRunning then
  CloningClosefiles; // Clears all variables and file handles

  {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS HELPER: Executing CFRunLoopRunInMode')); {$ENDIF}

  repeat
    reason := CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
    // do your thing here
  until (reason<>kCFRunLoopRunTimedOut) or TerminateHelper;

  {$IFDEF DEBUG} NSLog(NSSTR('SMJOBBLESS HELPER: BEYOND CFRunLoopRun (Terminating)')); {$ENDIF}

  CFRelease(receivingPort);
  ExitCode:=0;
end.

De procedure, welke ik eerder beschreef, voor het aanmaken van een communicatiepoort is eenvoudig.
Simpelweg CFMessagePortCreateLocal met de juiste parameters aanroepen, en je hebt een poort.
Maar goed, alleen een poort hebben is niet zinvol. Geen van de berichten komt aan, en de applicatie sluit vlot omdat het “klaar” is – we hebben geen mechanisme om de applicatie te laten draaien zodat het berichten kan ontvangen.

We hebben dus een loop nodig die in principe eindeloos door blijft lopen – of tot we het vertellen te stoppen.

Hiervoor gaan we een zogenaamde CFRunLoop (CFMessagePortCreateRunLoopSource) maken, en we registreren een “observer” (CFRunLoopAddSource), welke in de gaten houdt of er berichten binnenkomen voor ons Hulp Programma. Je kunt hiervoor CFRunLoop gebruiken, met de functie CFRunLoopRun, een Core Foundation functie welke een loop levert. Echter dexze functie gaat een loop draaien in de huidige thread. Omdat onze applicatie echter een “single thread” applicatie is, wil dit effectief zeggen dat de applicatie “vast” loopt in de CFRunLoopRun. Je kunt niets meer doen buiten de loop.

Na wat prullen en veel zoekwerk vond ik gelukkig CFRunLoopRunInMode waarme we onze eigen loop kunnen maken:


1
2
3
4
repeat
  reason := CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
  // do your thing here
until (reason<>kCFRunLoopRunTimedOut) or TerminateHelper;

Vergeet niet dat we relatief prompt een antwoord terug moeten sturen naar onze Hoofd Applicatie, dus “do your thing here” moet zorgvuldig opgebouwt worden zodat de individuele stappen in dat stuk kort genoeg zijn om berichten te ontvangen en beantwoorden.

Hier een korte toelichting van de functies die ik gebruikt heb;

CFMessagePortCreateRunLoopSource maakt een RunLoop Source object voor ons.


1
2
3
4
5
// Objective-C
CFRunLoopSourceRef CFMessagePortCreateRunLoopSource(CFAllocatorRef allocator, CFMessagePortRef local, CFIndex order);

// Pascal - unit CFMessagePort
function CFMessagePortCreateRunLoopSource( allocator: CFAllocatorRef; local: CFMessagePortRef; order: CFIndex ): CFRunLoopSourceRef; external name '_CFMessagePortCreateRunLoopSource';

 

CFRunLoopAddSource voegt het CFRunLoopSource object toe aan de RunLoop.

De parameter “rl” (de “RunLoop”) kunnen we met de functie CFRunLoopGetCurrent bepalen voor ons Hulp Programma.
De volgende parameter, “source“, is het resultaat van de eerder gemaakt CFMessagePortCreateRunLoopSource call.
En als laatste; de parameter “mode” bepaald hoe de CFRunLoop gaat draaien, en we kiezen hier “kCFRunLoopDefaultMode“.


1
2
3
4
5
// Objective-C
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);

// Pascal - Unit CFRunLoop
procedure CFRunLoopAddSource( rl: CFRunLoopRef; source: CFRunLoopSourceRef; mode: CFStringRef ); external name '_CFRunLoopAddSource';

 

CFRunLoopRunInMode draait de RunLoop tot er een reden is om de loop te verlaten, en geeft ons de reden waarom de loop werd verlaten.

Ook hier gebruiken we weer de parameter “mode” (kCFRunLoopDefaultMode), een korte interval (0.1 seconde), en geven we aan niet terug te keren nadat het e.e.a. is afgehandeld.

Deze laatste parameter vraagt om nog wat geëxperimenteer, om te zien wat een “true” waarde gaat doen.

Er zijn overigens 4 mogelijke return waarden; kCFRunLoopRunFinished (RunLoop afgelopen), kCFRunLoopRunHandledSource (een specifieke source werd afgehandeld),  kCFRunLoopRunStopped (CFRunLoopStop werd aangeroepen), en kCFRunLoopRunTimedOut (timeout).
Dat laatste is het criterium om onze eigen loop te verlaten.


1
2
3
4
5
// Objective-C
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

// Pascal - unit CFRunLoop
function CFRunLoopRunInMode( mode: CFStringRef; seconds: CFTimeInterval; returnAfterSourceHandled: Boolean ): SInt32; external name '_CFRunLoopRunInMode';

 

Steppen in onze Eigen Loop

Nu we onze eigen loop hebben, kunnen we hierin een mechanisme bedenken hoe we bijvoorbeeld in deze stappen onze disk gaan kopiëren.

Het moge duidelijk zijn dat een disk kopiëren enkele minuten gaat kosten, wat dus veel te lang is om tussendoor berichten te ontvangen en te lang is om tijdig een antwoord terug te sturen.

Vergeet niet: een prompt antwoord sturen is wenselijk zodat ons Hoofd Programma kan zien waar we zijn met het kopieer process, of zelfs kan zien dat het Hulp Programma überhaupt nog reageert.

In mijn voorbeeld, moest ik het het disk-kopieerproces, splitsen in kleinere stappen, zodat tussen de stappen door berichten afgehandeld kunnen worden.

Normaal gesproken zou ik zo een disk kopiëren:

  1. Open het disk device
  2. Open het backup bestand
  3. Lees een block van de drive
  4. Schrijf dat block naar het backup bestand
  5. Herhaal stappen 3 en 4 tot we het einde van de disk bereikt hebben
  6. Sluit het disk device
  7. Sluit het backup bestand

Om maar te zwijgen over het feit dat we tussen de stappen door willen weten hoe ver we zijn met het kopiëren …

Dit helpt ons meteen met het definiëren van de individuele stappen.

In mijn Hulp Programma, gebaseerd op de waarde van een globale variabele met de naam “Status”, bepalen we welke stap moet worden uitgevoerd in de “repeat … until” loop.

 

Als voorbeeld van onze loop:

  • Status = -1 is de standaard waarde; we staan dan eigenlijk te wachten op een commando (bijvoorbeeld het “DOSOMETHING” bericht van ons communicatie protocol).
  • Zodra we het “DOSOMETHING” bericht ontvangen zetten we Status = 0.
  • Zodra we het “STATUS” bericht ontvangen, geven we de waarde van “Status” terug.
  • Als Status = 0 in de repeat-loop, open dan de disk en het backup bestand (global variable in het Hulp Programma voor de file handles) en zet Status = 1.
  • Als Status>0 in de repeat loop, less dan een enkele block van de disk en schrijf deze naar het backup bestand. Verhoog vervolgens de waarde van Status met het aantal gelezen bytes.
  • Als tijdens het lezen van de disk, het einde van de disk is bereikt; sluit dan de disk en het backup bestand en zet Status weer op -1.

 

Merk op: Ik heb gemerkt dat ik gerust een aantal blokken in de loop kan lezen (zeker 4 blokken van 16Mb per stap) – verder testen moet aantonen of er een snellere of beter waarde of methode is voor het kopiëren van een disk.

Uiteraard is dit maar een voorbeeld, en design en implementatie voor jouw applicatie is “up to you”.

Download mijn Voorbeeld Project

Ik heb mijn voorbeeld project hier beschikbaar gemaakt voor download – je moet wel zelf de Hoofd Applicatie en het Hulp Programma compileren. Lees de instructies zorgvuldig!

Ik heb de code ook op GitHub gezet: macOS-SMJobBless-Lazarus-Pascal

DOWNLOAD - Lazarus Pascal MacOS SMJobBless Voorbeeld 

Platform: Mac OS X
Bestand: Lazarus-Pascal-MacOS-SMJobBless.zip
Versie: 1.0
Omvang: 145.0 KiB
Datum: 17 mrt 2019
 Download Nu 

Voor je aan de slag gaat met het Voorbeeld Project

Dit project is gemaakt en getest met Lazarus 2.1.0 r60620M FPC 3.0.4 x86_64-darwin-cocoa (alpha) (van SVN), onder MacOS 10.14.3.
Zowel Hulp Programma als Hoofd Applicatie moeten als 64 bit applicatie gecompileerd worden (wat automatisch het geval is in het voorbeeld project).

Om het voorbeeld project te testen, moet je een valide Apple Developer ID hebben zodat je Hoofd Applicatie EN Hulp Programma kunt signeren.

In de volgende bestanden, vervang “XXXXXXXXXX” met jouw Developer ID:

SMJobBlessTest/project1.app/Contents/Info.plist
SMJobBlessTest/Helper/SMJobBlessHelper-Info.plist

In  SMJobBlessTest/prepareproject1.sh moet je “John Doe (XXXXXXXXXX)” met jouw Developer naam en ID vervangen.

 

Om het project te testen:

  1. Open het com.tweaking4all.SMJobBlessHelper.lpi project in Lazarus en compile het met “Build” (NIET “run”!).
  2. Open het project1.lpi in Lazarus en compile het met “Build” (NIET “run”!).
  3. In Terminal, ga naar de “SMJobBlessTest” directory en run het “./prepareproject.sh” script.
    Het script kopieert de binaries, strit ze, en signeerd ze.
  4. In Terminal kun je optioneel de app bundle (project1.app) verifieren met “./SMJobBlessUtil.py check project1.app“.
    Foutmeldingen wijzen vaak op type fouten in de plist bestanden en/of signeer fouten.
  5. Open Console (Applications/Utilities) en zet de search filter op “SMJOBBLESS” – zodat je de NSLog berichten van Hoofd Programma en Hulp Programma kunt zien.
  6. Dubbel klik nu project1.app in de “SMJobBlessTest” directory.
    Je ziet een paar knoppen en dit is wat ze doen:
    – “Initialize” wordt gebruikt voor het installeren van het Hulp Programma
    – “Start Complicated” start de “taak” en blijft de tijd in de Console log zetten
    – “Is Active?”” laat zien of de “taak” loopt
    – “Abort Complicated” verzoekt om de “taak” te stoppen
    – “Last Error” geeft alleen maar een dummy antwoord
    – “Get Version” vraagt de huidige versie van het Hulp Programma
    – “Quit Helper” zal het Hulp Programma stoppen

 

Opruimen als je uitgespeeld bent:

Verwijder het Hulp Programma in Terminal met:


$ sudo rm /Library/PrivilegedHelperTools/com.tweaking4all.SMJobBlessHelper

Kill een draaiend Hulp Programma in Terminal met (voor het geval iets goed fout ging):


1
2
$ ps -ax | grep com.tweaking4all
$ sudo kill -9 <PID></strong>

Waar <PID> het PID van de “ps” regel komt waar je de volledige naam van het Hulp Programma ziet (com.tweaking4all.SMJobBlessHelper).

 

Uiteindelijk hoop ik dat iemand anders hier ook wat aan heeft. Het heeft erg veel tijd en moeite gekost om dit allemaal bij elkaar te zoeken en aan de gang te krijgen. Nogmaals, ik ben geen expert en leer graag van suggesties en ideeën van anderen – post ze dus vooral hieronder.

 

Maffe Signing Code Details

Omdat ik best veel moeite moest doen om de info te vinden voor een goede verklaring van de rare signeercodes die we gebruikt hebben voor het signeren van zowel Hoofd- als Hulp Programma, besloot ik de codes hier even te plaatsen voor het geval iemand interesse heeft.

Hieronder een voorbeeld hoe de lijst werk, op basis van de code  1.2.840.113635.100.6.2.6:

1 = iso, 
    .2 = member-only, 
        .840 = us, 
            .113635 = Apple, 
                .100 = Apple Security, 
                    .6 = Apple Certificate Extensions, 
                        .2 = intermediate marker, 
                            .6 = Developer ID CA

En al dit is gebaseerd op deze lijst:

1 iso
    .2 member-only
        .840 us
            .113635 Apple OID
                .100 apple Data Security
                    .1 apple Trust Policy
                    .2 apple Security Algorithm
                    .3 apple Dot Mac Certificate
                    .4 apple Extended Key Usage
                        .2 iChat Signing
                        .3 iChat Encryption
                        .4 System Identity
                        .8 Safari Extensions
                        .9 3rd Party Mac Dev Installer
                        .11 Mac Provisioning Profile
                        .12 CSR signing
                        .13 Developer ID Installer
                        .14 Passbook signing
                        .15 Website Push Notification
                        .16 Profile Signing
                        .17 QA Profile Signing
                    .5 apple Certificate Policies
                        .1 Apple Signing
                        .2 .Mac
                        .3 Apple Dev Connection
                        .6.1 Mac App Store Receipt
                        .7.1 AppleID Sharing
                        .12 Mobile Store
                    .6 apple Certificate Extensions
                        .1 Code Signing
                            .2 iOS Software Dev Signing
                            .4 iOS Software Submission Signing
                            .5 Safari Extensions
                            .7 Software Submission Signing
                            .8 Mac Installer Packages
                            .9 Mac App Store App
                            .10 Mac App Store Installer Package
                            .11 Mac App Store Receipt Signing
                            .12 Software Development Signing
                            .13 Developer ID Applications
                            .14 Developer ID installers
                            .16 Apple Pay/Passbook
                            .17 Website Push Notification
                            .18 Developer ID Kexts
                            .21 OS X Server Authentication
                            .25 TestFlight Distribution (25.1, 25.2)
                            .26 Enhanced Passbook Certificates
                            .32 Apple Pay Merchant Services
                            .46 Apple Pay Merchant Services
                            .39 Provisioning Encryption
                        .2 Intermediate Marker
                            .1 WWD relations CA
                            .6 Developer ID CA
                        .3 Apple Push Notification Service
                            .1 APS Dev
                            .2 APS Production
                            .3 VoIP Push
                            .4 VoIP Push
                            .5 VoIP Push
                            .6 WatchKit

 

Referenties

Merk op: deze zijn allen in het Engels!

 

Donatie opties


Donaties worden zeer gewaardeerd maar zijn echt niet verplicht. Donaties worden gebruikt om de web-hosting kosten te dekken, hard- of software aan te schaffen voor projecten die we hier bespreken, of zo nu en dan voor een pilsje of een kopje koffie!

Reacties


Er zijn nog geen reacties geplaatst.
Je kunt jouw eigen opmerkingen plaatsen m.b.v. dit formulier, of een reactie op een bestaande opmerking plaatsen door op de "Beantwoorden" knop te klikken.



Jouw opmerking ...

Vriendelijk verzoek om hier geen lange bestanden te posten (zoals source codes, log bestanden of config bestand). Gebruik het Forum hiervoor.

Deel:
*
*
Stuur me een notificatie van nieuwe opmerkingen (email).
       Je kunt jouw RSS reader gebruiken op opmerkingen te volgen.


Tweaking4All gebruikt de gratis Gravatar dienst voor de weergave van Avatars.
Tweaking4All deelt jouw email adressen NOOIT.