Montag, 6. August 2018

PowerShell CmdLet reflecten

Meistens muss ich alles ganz genau wissen: Z.B. wie ein spezielles PowerShell-CmdLet implementiert ist.
Mittels des folgenden kleinen Skriptes​ kann man ganz schnell die implementierende DLL zu einem CmdLet finden und notfalls auch schon mal einen decompiler starten.​
Die Inspiration kommt von OISIN GREHAN​.

<#  
.SYNOPSIS  
    Run reflection on a given commandlet
.DESCRIPTION  
    Run reflection on any CmdLet
.NOTES  
    This code was heavily inspired from OISIN GREHAN, see http://www.nivot.org/post/2008/10/30/ATrickToJumpDirectlyToACmdletsImplementationInReflector
.Example
    Get-Command Get-ChildItem | Reflect-Cmdlet -Reflect ShowDllOnly
.Example
    Reflect-Cmdlet -CmdLet (Get-Command Get-ChildItem) -Reflect ShowDllOnly
#>
[CmdletBinding()]
param(
    [Parameter(Position=0, 
        Mandatory=$true, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
        HelpMessage = "The CmdLet to reflect")]
    [Management.Automation.CommandInfo]$CmdLet,
 
    [Parameter(HelpMessage = "Which reflector to use.. ")]
    [ValidateSet("ShowDllOnly", "Reflector", "JustDecompile", "ILSpy")]
    [string]$Reflect = "ShowDllOnly"
)
      
# resolve to command if this is an alias  
while ($CmdLet.CommandType -eq "Alias") {  
    $def = $CmdLet.definition;
    Write-Verbose "$CmdLet is an alias. Using Definition: $def";
    $CmdLet = Get-Command $def
}  
Write-Verbose "Reflecting $CmdLet.";
       
$name = $CmdLet.ImplementingType      
$DLL = $CmdLet.DLL  
if($DLL -eq $null) {
    Write-Warning "$CmdLet is not implemented in any DLL. Possibly a script?";
    if($CmdLet.Path -ne $null) {
         Write-Warning "Have a look at: $($CmdLet.Path)"
    }
    #$CmdLet | gm
    Exit;
}
 
Write-Verbose "Type:$name, DLL:$DLL";
switch ($Reflect) {
    "ShowDllOnly" {
        Write-Output "$CmdLet is implemented in $name in the dll:$DLL";
    }
    "Reflector" {
        if (-not (Get-Command reflector.exe -ErrorAction SilentlyContinue)) {  
            throw "Reflector.exe is not in your path."
        }  
        Write-Verbose "Starting Reflector.";
        reflector /select:$name $DLL
    }
    "JustDecompile" {
        $regKey = Get-Item HKCU:\Software\Telerik\JustDecompile -ErrorAction SilentlyContinue
        if($regKey -eq $null) {
            throw "It seems JustDecompile is not installed."
        }
        $exe = $regKey.GetValue("ExecutablePath") ;
        Write-Verbose "invoking $exe";
        &$exe """$DLL""" ; #TODO: select the right type...
    }
    "ILSpy" {
      if (-not (Get-Command ilspy.exe -ErrorAction SilentlyContinue)) {  
            throw "ilspy.exe is not in your path."
        }  
      Write-Verbose "Starting ILSpy.";
      ilspy $DLL /navigateTo:T:$name
    }
}

Mittwoch, 25. Juli 2018

ExecuteOrDelayUntilScriptLoaded oder doch lieber ExecuteFunc?

Ich falle immer wieder über den Unterschied zwischen ExecuteOrDelayUntilScriptLoaded und ExecuteFunc.
ExecuteOrDelayUntilScriptLoaded führt einen callback aus, sobald eine abhängige Datei geladen ist. Allerdings führt dies nicht dazu, dass die abhängige Datei geladen wird.

Der code:

SP.SOD.executeOrDelayUntilScriptLoaded(function(){
    alert('Hallo, Welt.');
}, 'sp.js');

öffnet also den alert nur, wenn die sp.js benötigt und geladen wurde. Wenn die sp.js nie geladen wird, wird der alert nie erscheinen.

ExecuteFunc hingegen führt einen callback aus, nachdem eine abhängige Datei geladen ist. Die abhängige Datei wird dabei geladen, wenn sie nicht schon geladen war. Dies führt dazu, dass der callback nicht ausgeführt wenn die Datei vorher schon geladen war.

Der code:

SP.SOD.executeFunc('sp.js', null, function() {
    alert('Hallo, Welt!');
});

öffnet also den alert nur, wenn die sp.js vorher noch nicht geladen war..

Es bleibt die Frage: Wie stelle ich jetzt also sicher, dass mein code "nach" der sp.js aufgerufen wird und gleichzeitig, dass die sp.js auf jeden Fall geladen wird?

ExecuteOrDelayUntilSciptLoaded liefert einen bool zurück, der angibt ob die abhängige Datei schon angefordert war. D.h. falls ExecuteOrDelayUntilScriptLoaded false liefert sind wir im "delay"-Teil  der Funktion gelandet und warten darauf, dass die abhängige Datei geladen wird...

Der Code:

if(!SP.SOD.executeOrDelayUntilScriptLoaded(function(){
    alert('Hallo, Welt.');
}, 'sp.js')) {
    // sp.js war nocht nicht angefordert...
    SP.SOD.executeFunc('sp.js', null, function(){});
}

wird den alert öffnen, wenn die sp.js geladen ist und gleichzeitig die sp.js anfordern, falls diese bisher noch nicht angefordert war.

Donnerstag, 19. Juli 2018

SharePoint "nativehr"-Fehler erklärt

Jeder SharePoint-Entwickler kennt Fehler in der Form:
0x80070718  Your changes could not be saved because this SharePoint Web site has exceeded the storage quota limit.
You must save your work to another location.  Contact your administrator to change the quota limits for the Web site.
Ein Kunde frage neulich: "Woher kommt eigentlich die Zahl (0x80070718) und was bedeutet die?"

Die Frage ist schnell erläutert: Es handelt sich um die hex-Darstellung des HRESULT, welcher der Auslöser des Fehlers war.
Über HRESULT gibt es reichlich Informationen. Am besten ist aber, finde ich, diese (generelle Erläuterung für HRESULT und Facilities) in Kombination mit dieser (liste der Fehler-Codes).

Kurz erläutert besteht ein HRESULT aus 32 Bit, wobei die hohen 5 Bit wie folgt bezeichnet sind:
  • S (1 bit): Severity. If set, indicates a failure result. If clear, indicates a success result.
  • R (1 bit): Reserved. If the N bit is clear, this bit MUST be set to 0. If the N bit is set, this bit is defined by the NTSTATUS numbering space (as specified in section 2.3).
  • C (1 bit): Customer. This bit specifies if the value is customer-defined or Microsoft-defined. The bit is set for customer-defined values and clear for Microsoft-defined values.
  • N (1 bit): If set, indicates that the error code is an NTSTATUS value (as specified in section 2.3), except that this bit is set. 
  • X (1 bit): Reserved. SHOULD be set to 0.
Es folgen 11 Bit "Facility" und anschließend 16 Bit für den eigentlichen Code.

Der Wichtige Punkt daran ist, dass Customer-Bit: Wenn dieses 0 ist, kommt die Meldung von Microsoft und kann in der o.a. Doku "nachgeschlagen" werden. (Ob das dann hilft, oder nicht...)

Für den angegebenen Fall (0x80070718): In Binär-Darstellung ist die Zahl 10000000000001110000011100010101 - und kann daraus einfach "abgezählt" werden:

SRCNXFacilityCode
10000000000001110000011100010101
Ein Fehler von Microsoft0x7 (7)0x715 (1813)

Wobei die Facility wie in [MS-ERREF] beschrieben einen den Fehler als FACILITY_WIN32 ("This region is reserved to map undecorated error codes into HRESULTs.") Fehler beschreibt und der Code, wie bei den SystemErrorCodes beschrieben einen ERROR_NOT_ENOUGH_QUOTA Fehler beschreibt.

Es handelt sich also um einen "generellen" Win32-Fehler, bei dem nicht genügend Quota vorhanden ist. Hat das geholfen? Nicht wirklich, denn das stand auch schon in der Meldung von SharePoint... Aber es ist erklärt...

Da "Abzählen" von Bits nicht immer einfach oder schnell ist, habe ich ein kleines PowerShell Skript geschrieben, dass die "Analyse" erstellen kann:

<#
  .SYNOPSIS
   Decode HResult (hr) codes.. or die trying...
 
  .EXAMPLE
   Decode-HResult 0x80070718
   Shows info about "Quota Exceeded"...
#>

[CmdletBinding()]
param(
    [Parameter(Position=0)]
    [int]$hr
)

function bitshift {
 # https://stackoverflow.com/questions/35116636/bit-shifting-in-powershell-2-0
    param(
        [Parameter(Position=0)]
        [int]$x,

        [Parameter(ParameterSetName='Left')]
        [int]$Left,

        [Parameter(ParameterSetName='Right')]
        [int]$Right
    ) 

    $shift = if($PSCmdlet.ParameterSetName -eq 'Left')
    { 
        $Left
    }
    else
    {
        -$Right
    }

    return [math]::Floor($x * [math]::Pow(2,$shift))
}

$i = [int]$hr
$hex = "0x$([Convert]::ToString($i, 16))"
$bin = [Convert]::ToString($i, 2)

write-verbose "code as int: $i"
write-verbose "code as hex: $hex"
write-verbose "code as bin: $bin"
write-verbose ""
write-verbose "decoding:"
write-verbose $bin.PadLeft(32, " ")
write-verbose "SRCNX|- facil -||---- code ----|"

## see https://en.wikipedia.org/wiki/HRESULT
## and https://msdn.microsoft.com/en-us/library/cc231198.aspx
## and https://docs.microsoft.com/en-us/windows/desktop/com/structure-of-com-error-codes

# lowest 2 bytes for code
$code = $i -band 0xffff
# then 11 bit for facility
$fac = $i -band 0x7ff0000
if($fac -ne 0) { $fac = (bitshift $fac -Right 16)}
# 2 bit reserved, then 1 bit for customer 
$cust = $i -band 0x20000000
if($cust -ne 0x0) { $cust = (bitshift $cust -Right 29)}
# 1 bit reserved (severe?) then 1 bit success (probably always 1?)
$nok = $i -band 0x80000000
if($nok -ne 0) { $nok = 1 }

write-host "hr = $hex"
write-host "Severity: $( if($nok -ne 0){"Fail"} else {"Success"} )"
write-host "code: 0x$([Convert]::ToString($code, 16))"
write-host "facility: 0x$([Convert]::ToString($fac, 16))"
if($cust -eq 0) {
 write-host "the code was Microsoft-defined. "
 Write-host "  So you can lookup facility ($fac) under https://msdn.microsoft.com/en-us/library/cc231198.aspx"
 write-host "  and the code ($code) under https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes"
} else {
 write-host "the code was Cutomer-defined"
}


Mittwoch, 18. Juli 2018

Hat einer 'TokenReplacementFileExtensions' überschrieben?

In einer existierenden SharePoint-Solution habe ich heute eine neue Layouts-Page angelegt und mich anschließend mit dem folgenden Fehler herumgeschlagen:

Error CS0234 The type or namespace name 'Expressions' does not exist in the namespace 'System.Web.UI.WebControls' (are you missing an assembly reference?)

Meine Verwirrung war recht umfassend, da ich das Wort "Expressions" nicht finden konnte, das Problem aber offensichtlich an der von mir neu hinzugefügten aspx-Seite lag.

Um meine längliche Suche abzukürzen: Die Solution wurde vor langer Zeit erstellt um einen WebService im SharePoint zu hosten. Vermutlich hat sich der Ersteller an eine der vielen Anleitungen zum Thema (z.B. diese..) gehalten und den folgenden Text in der csproj erfasst:

<TokenReplacementFileExtensions>svc</TokenReplacementFileExtensions>

Dieses Vorgehen findet sich in fast allen Anleitungen... Leider wird dabei oft übersehen, dass diese Eigenschaft nicht "aus dem Nichts" kommt, sondern vorher, an anderer Stelle, schon definiert sein muss. Die Doku sagt dazu:
Although tokens can theoretically be used by any file that belongs to a SharePoint project item included in the package, by default, Visual Studio searches for tokens only in package files, manifest files, and files that have the following extensions:
  • XML
  • ASCX
  • ASPX
  • Webpart
  • DWP

Diese "Voreinstellungen" werden also überschrieben, wenn man strikt den Anleitungen folgt. Eine bessere Möglichkeit wäre es die die TokenReplacementFileExtensions zu ergänzen, statt zu überschreiben:

<TokenReplacementFileExtensions>$(TokenReplacementFileExtensions);svc</TokenReplacementFileExtensions>


Dienstag, 10. Juli 2018

SharePoint redirect an ein Add-In

Aktuelle Frage: Wie erstelle ich in SharePoint einen Link auf ein Add-In?

Klar, im Webseiteninhalt taucht eine nette Kachel auf die den Link beinhaltet. Aber wie erstellt man einen generellen Link z.B. auf einer eigenen Seite?

Die Antwort ist die 'AppRedirect.aspx'. Diese bekommt die ID der add-in instanz übergeben etwa so: 'https://mein.sharepoint.de/_layouts/15/appredirect.aspx?instance_id={A3F2D157-721A-42B9-85F6-B98FBF19D40B}'.

Aber wie kommt man an die ID - wenn man vielleicht nicht gerade auf die ensprechende Inhaltsseite gucken und die ID "abschreiben" kann?

Einfach - die Lösung findet sich im SP.AppCatalog-Objekt:
function redirectToApp(appName){
  var ctx = SP.ClientContext.get_current(),
    web = ctx.get_web(),
    appInstances = SP.AppCatalog.getAppInstances(ctx, web);
  ctx.load(appInstances);

  ctx.executeQueryAsync(function(){
    var apps = appInstances.getEnumerator(),
      app;
    while(apps.moveNext()){
      app = apps.get_current();
      if(app.get_title() === appName){
        break;
      }
    }
    if(!app){
      throw "No app found:"+appName
    }
    var redirectUrl = _spPageContextInfo.webAbsoluteUrl.replace(/\/?$/, '/'+_spPageContextInfo.layoutsUrl) + 
      '/appredirect.aspx?instance_id={' +app.get_id() + '}';
    
    document.location = redirectUrl;
  }, function(){ throw 'Error loading app-instances..'; });
}

Samstag, 30. Juni 2018

Bereinigen von virtuellen maschinen

Als SharePoint-Entwickler arbeite ich häufig in Virtuellen umgebungen. Diese sollten gerne möglichst "platzsparend" gelagert werden können - das ist auch gut, wenn mal eine Maschine an einen Kollegen "abgetreten" werden soll.

Mein akutelles Vorgehen dabei ist das folgende:
  • Bereinigung des WinSxS-Ordners mittles "Dism.exe /online /Cleanup-Image"
    • Für Service-Packs mit dem Parameter "/SPSuperseded"
    • Für Komponenten mit dem Parameter "/StartComponentCleanup /ResetBase"
  • Bereinigung der Laufwerke mittels "cleangr.exe"
    • Um den cleanmgr ohne Rückfragen zu starten gibt es die Parameter "/sageset" und "/sagerun". (Entgegen der Dokumentation gibt es nur 9999 verschiedene Speicherplätze und nicht 65535). Die Einstellungen, die mit "sageset" vorgenommen werden, werden in der registry gespeichert und daher auch einfach mit Powershell angelegt werden.
  • Defragmentierung des Datenträgers mit ultradefrag
    • Für optimale Ergebnisse verwende ich die Parameter "--optimize" und "--repeat"
  • "Nullen" der nicht mehr verwendeten Blöcke des Datenträgers mit sdelete
    • Für das "einfache" "Nullen" der Blöcke verwende ich den Parameter "-z"
    • Dieser Schritt führt - zusammen mit der defragmentierung - zu einem besseren Ergebnis beim komprimieren der virtuellen Festplatte

Komplett sieht das Skript dann so aus:
Dism.exe /online /Cleanup-Image /StartComponentCleanup /ResetBase
Dism.exe /online /Cleanup-Image /SPSuperseded
Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches | `
              %{ $_| Set-ItemProperty -Name "StateFlags1234" -Value 2 }
cleanmgr.exe /sagerun:1234
udefrag.exe --optimize --repeat C:
sdelete.exe -q -z C:

Donnerstag, 28. Juni 2018

SharePoint Listeneinstellungen öffnen ohne Ribbon...

Aktuell bestand das Problem dass für einige SharePoint-Listen die Einstellungsseite geöffnet werden sollte, der Ribbon aber nicht verfügbar war.
Einstellungen von Listen erfolgen ja über die Seite "listedit.aspx" im layouts, stellt sich nur die Frage wie man schnell an die ID der liste kommt, da diese ja als query-parameter übergeben werden muss.


Meine Antwort ist in SharePoint ab 2010 lauffähig:

document.location = 
    document.location.href.replace(document.location.pathname, '') + 
    _spPageContextInfo.webServerRelativeUrl.replace(/\/?$/, '/' + 
    (_spPageContextInfo.layoutsUrl || '_layouts') + 
    '/listedit.aspx?List=' + 
    encodeURIComponent(_spPageContextInfo.pageListId))


Einfach in der Konsole einer Listenseite (z.B. "AllItems.aspx") ausführen.