anti-idle

o co cho?

pracując na kilq terminalach mam często problem z blokadą ekranu i wymuszeniem hasła – przez agresywne GPO. wpisywanie hasła za każdym razem zabiera czas – zwłaszcza jak tych sesji jest otwartych kilka i trzeba się do nich dostać.

wystarczy ruszyć myszką albo nacisnąć klawisz, żeby idle-timer się wyzerował. oba te zdarzenia można wywołać ze skryptu, a więc – jest rozwiązanie q:

skrypt

gotowy skrypt umieściłem na GH.

jest również wersja skompilowana za pomocą PS2EXE.

eN.

w-files w audio

blog od dłuższego czasu przymiera… jeśli chcesz się dowiedzieć czemu…

ostatnio miałem przyjemność gościć w Drafcie Rozmowy więc mogę uznać, że to taki wpis w wersji audio… trochę nawiązując do (niedoszłego) tematu rozmowy – zauważalny jest trend przejścia ze słowa pisanego do multimediów … ale o tym będzie następnym razem. tym razem rozmawialiśmy o tym, co robimy na codzień (i kochamy) – czyli o IT.

podcast (ten i wiele innych!) dostępny na wielu platformach, ja pozwolę sobie podlinkować ten googlowy, ale znajdziecie w swoim ulubionym playerze pod hasłem 'draft rozmowy’ (:

eN.

Passwordless dla Microsoft Account

niedawno Microsoft ogłosił funkcję 'passwordless’ dla kont 'Microsoft Account’ – czyli tych nie-firmowych/nie-szkolnych. wszystkie arty dot. braq haseł rozpływają się w zaletach tego rozwiązania i ah, oh [fapfap] … więc daruję sobie powtarzanie zalet braq hasła. funkcję włączyłem i … obniżyłem sobie bezpieczeństwo dostępu.

trzeba jasno odgrodzić passwordless dla firm i funkcje Windows Hello for Business, który zakłada MultiFactor Auth – z naciskiem na ’Mutli’, od passwordless dla kont MS. zastanawiałem się jak to będzie praktycznie zrealizowane, włączyłem –  i osobiście mnie to trochę przeraża – ponieważ z MFA wracamy do Single Factor Auth, którym jest nasz smartfon. nie spodziewam się, aby wiele osób miało na kontach dane warte ucięcia komuś kciuka [i przechowywania w odpowiednich warunkach], ale mimo wszystko logowanie, w którym JEDYNYM czynnikiem jest kliknięcie na ekranie telefonu, budzi u mnie alarm. jakiś czas temu, o ile pamiętam, była opcja dodatkowego zabezpieczenia apki Microsoft Authenticator PINem. dziś znajduję w opcjach jedynie 'require biometric or PIN’ ale bez możliwości wyboru – wygląda na to, że ustawienie pobierane jest z systemu (sprawdzałem na Android) – a więc czy będzie to odcisk, czy PIN – będzie to TEN SAM odcisk/PIN, co do reszty. ciekawy jestem czy ta formuła faktycznie zadziała – dowodem będzie, że przez najbliższe kilka lat będzie funkcjonować w tej postaci, będzie używana, a w prasie nie zaczną się pojawiać arty o tym jak to wypłynęła porcja danych/zdjęć/doqmentów bo ktoś zgubił smartfona. imho bardzo szybko pojawi się nowa wersja apki lub ustawienie, które doda jakiś drugi czynnik – może PIN, może kod SMS, czy choćby wybór obrazka. co ciekawe w ustawieniach konta nadal widzę 'Two-step auth ON’ – pomimo, że loguję się teraz tylko przykładając palec do ekranu.

niezależnie od mojej subiektywnej opinii, każdy powinien zrewidować swoje ustawienia haseł i upewnić się:

  • czy na pewno mamy skonfigurowane dodatkowe metody uwierzytelnienia?
  • czy je pamiętamy? [np. czy znamy odpowiedzi do pytań weryfikacyjnych]
  • czy ustawienia tych metod nie zapętlają się?

jeśli naszą drugą metodą jest adres email, który ma włączone MFA oparte o tą samą apkę – to tracąc telefon nie zalogujemy się nigdzie. powinniśmy mieć realną alternatywę inaczej czeka nas długie przebijanie się przez support. jeśli coś takiego wydarzy się gdy nie mamy czasu, będziemy w kropce. każdy powinien poświęcić czas chociaż raz, i 'na niby’ zgubić telefon. wyłączyć go i sprawdzić gdzie jest w stanie się zalogować, czego braqje, ile to trwa czasu. w firmach na podobne scenariusze powinny istnieć procedury, ale sprawę ułatwia, że zazwyczaj jest kilq administratorów, więc w większości scenariuszy są sobie w stanie pomóc na wzajem. prywatnie, nas czeka przebijanie się przez linie wsparcia – warto przynajmniej spróbować, żeby oszacować ile to może trwać czasu i jak w ogóle taka procedura wygląda.

z jednej strony firmy dwoją się i troją, żeby było bezpieczniej a zarazem łatwiej… wygoda na pewno się zwiększa – ale w momencie kiedy coś się sypnie, zaczynają się schody. i wcale nie jest łatwo wymyśleć i skonfigurować swoje konta tak, aby faktycznie odzyskać awaryjnie dostęp szybko i bezpiecznie.

ciekaw jestem opinii – bo na razie nie trafiłem na te negatywne czy krytyczne. moja jest dość mieszana – niby wygodnie, ale [w przypadku kont prywatnych] na pewno nie bezpieczniej!

eN.

 

get-member – jak używać?

potęgą PowerShell jest jego wszech-obiektowość. zrozumienie pracy na takich obiektach to połowa sukcesu. dlatego kiedy coś nie działa lub zachowuje się inaczej niż spodziewanie – używa się get-member [lub po prostu gm]. drugim podstawowym mechanizmem jest oczywiście potokowanie – pipelining. i tak najprostszym sposobem sprawdzenia 'co kryje w sobie obiekt’ jest wykonanie

$something|gm

jest jednak pewien … no właśnie – niuans? niby niuans ale de facto istota funkcjonowania mechanizmów PS związany z tym, czym tak na prawdę jest potokowanie. prosty przykład:

$array=@(1,2)
$array|gm

czego intuicyjnie będziesz oczekiwał(a)? czy get-member zwróci informacje o zmiennej typu 'tablica’? oczywiście nie – zwróci opis zmiennych integer:

TypeName: System.Int32

Name MemberType Definition
---- ---------- ----------
CompareTo Method int CompareTo(System.Object value), int CompareTo(int value), int IComparable.CompareTo(System.Object obj), int IComparable[int].Comp...
Equals Method bool Equals(System.Object obj), bool Equals(int obj), bool IEquatable[int].Equals(int other)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
[...]

to zapewne nie jest zaskoczenie, ponieważ każdy zna to z praktyki. jak się nad tym jednak zastanowić, to nie jest oczywiste – czemu w wyniq dostaje się wynik get-member dla elementów tablicy a nie samej zmiennej tablicowej? ponieważ tak właśnie działa potok [pipe]. de facto jest to skrótowa wersja takiegoż zapisu w klasycznych językach:

foreach ($element in $array) {
  get-member -inputObject $element
}

i teraz wszystko jasne! tak bardzo uzależniamy się od automatyki PS i używania potoków, że czasem można zapomnieć, że to nie jest ani jedyny sposób użycia, ani – jak widać na tym przykładzie – niekoniecznie prawidłowy. jeśli więc chcemy zbadać konkretnie obiekt tablicowy, a nie jego elementy, wystarczy zaniechać potokowania:

get-member -inputObject $array

TypeName: System.Object[]

Name MemberType Definition
---- ---------- ----------
Count AliasProperty Count = Length
Add Method int IList.Add(System.Object value)
Address Method System.Object&, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Address(int )
Clear Method void IList.Clear()
Clone Method System.Object Clone(), System.Object ICloneable.Clone()
CompareTo Method int IStructuralComparable.CompareTo(System.Object other, System.Collections.IComparer comparer)
Contains Method bool IList.Contains(System.Object value)
CopyTo Method void CopyTo(array array, int index), void CopyTo(array array, long index), void ICollection.CopyTo(array array, int index)
[...]

eN.

 

PS Borderlands: $null.. czy nie?

$null czy nie $null, o to jest pytanie

jak sprawdzić czy zmienna została prawidłowo zainicjowana czy może jest wyzerowana? sposobów jest oczywiście kilka, zależnie od kontekstu. ale ponieważ trafiłem na brzegową sytuację, kilka ciekawostek…

pierwsze pytanie – co to znaczy 'wyzerowana zmienna’? od razu przychodzi mi do głowy pytanie-killer, dobre na sprawdzenie sprytu początqjącego deva – 'ile wartości można zakodować na jedno-bitowej zmiennej?’. dość oczywistą odpowiedzią jest 'dwie – 0 i 1′, ale prawidłową jest 'trzy – zero, jeden i null’. [oczywiście przykład wyssany z palca i można z tym polemizować, ale chodzi o ideę]. pokazuje to pewien niuans w sprawdzaniu wartości zmiennej, który może nieźle namieszać – zmienna pusta [empty] to nie to samo co zainicjowana zmienna z wartością $null [o ile $null można nazwać wartością, ale nie chodzi o filozofowanie tylko praktyczne konsekwencje].

początkowo zatem używałem prostego porównania:

if($zmienna -eq $null) { }

potem dowiedziałem się, że $null powinien być zawsze po lewej stronie, ponieważ w specyficznych przypadkach zachowa się 'nieoczekiwanie’ – polecam poczytać na docsach. dla tego poprawnym zapisem jest:

if($null -eq $zmienna) {}

przeważnie jednak, ze względu na logikę działania skryptów, chcę sprawdzić czy coś nie jest $null LUB puste. zacząłem więc przez długi czas używać magii PowerShell:

if(-not $zmienna) { }

co w zasadzie powinno uniwersalnie sprawdzić zmienną. tego zapisu nie używam od dłuższego czasu – nie pamiętam jednak dokładnie jakie tutaj występowały aberracje, ale z jakiegoś powodu przesiadłem się na natywną funkcję .NET dla klasy string:

[string]::isNullOrEmpty($zmienna)

i bardzo długo tą funkcję uważałem za uniwersalny tester i myślałem że znalazłem moje panaceum… do czasu aż nie trafiłem na kolejne brzegowe sytuacje, które nieoczekiwanie zwracają głupotę. najpierw przykład oczywisty, tutaj nie ma co się dziwić:

PS> $emptyArray=@()
PS> $emptyArray.count
0
PS> [string]::isNullOrEmpty($emptyArray)
True

kolejny nadal dość oczywisty przykład, choć warto zwrócić uwagę na olbrzymią różnię pomiędzy 'tablicą pustą’ z poprzedniego przykładu, a tablicą zawierającą element $null – zwróć uwagę na ilość elementów w tablicy:

PS> $nullArray=@($null)
PS> $nullArray.count
1
PS> [string]::isNullOrEmpty($nullArray)
True

i tu doszedłem do PS Borderlands – granic swojej wiedzy i możliwości zrozumienia mechanizmów pod spodem. dalszych przykładów nie potrafię wyjaśnić – znam je z obserwacji:

PS> $nullArray=@($null,$null)
PS> $nullArray.count
2
PS> [string]::isNullOrEmpty($nullArray)
False

wychodzi na to że jedno nic to nadal nic, ale dwa nic to już coś. miałem na studiach matematykę nieskończoności, ale nigdy matematyki nicości, najwyraźniej muszę tą dziedzinę lepiej zgłębić (;

a dalej… to już kosmos. przypadek tak zawikłany, że zajęło mi masę czasu póki go wyizolowałem i umiałem pokazać na przykładzie. w przeszłości również trafiałem na podobne zachowanie, więc scenariuszy jest prawdopodobnie więcej, ale dopiero teraz postanowiłem to zrozumieć. opiszę, może znajdzie się harpagan, który to rozwikła…

poza granicami

jeśli funkcja implementuje 'ValueFromRemainingArguments’, typ zwracanego argumentu będzie zależny od tego czy się z tej funkcjonalności skorzysta. tak działa między innymi 'write-host’ – wywołanie polecenia:

PS> write-host tu sobie cos tam napiszemy bez zadnych cudzyslowiow
tu sobie cos tam napiszemy bez zadnych cudzyslowiow

bez cudzysłowiów – czyli de fakto polecenie powinno potraktować każdy wyraz jako oddzielą wartość kolejnego parametru. a jednak zinterpretował to jako pojedynczą wiadomość. to właśnie instrukcja ValueFromRemainingArguments upakowała wszystkie nienazwane wartości pod jedną zmienną. podobny myk stosuję w swoim write-log dzięki czemu mogę uzyskać podobny efekt. na tej też funkcji mogłem zbadać, że przy takim wywołaniu:

PS> write-log -message "to cos"
to cos

zmienna $message będzie typu [string]. ale przy takich wywołaniach:

PS> write-log "to cos"
to cos
PS> write-log to inne cos
to inne cos

zmienna $message jest typu [`list] [nie pytajcie skąd ten backtick w nazwie typu – bardzo dziwaczny to zapis, dodatkowa przyprawa w tym i tak dziwnym przypadq]. to pierwsza ważna różnica, ponieważ problem pojawia się właśnie przy tym typie. a że zazwyczaj ułatwiamy sobie życie i jeśli można ’-message’ nie pisać, to go nie piszemy, scenariusz więc dość codzienny.

jeśli jako wiadomość przekaże się obiekt, to zostanie stworzona lista zawierająca obiekty. można to zasymulować tak:

PS> $przekazaneWartosci = new-object System.Collections.Generic.List[system.object]
PS> $mojObiekt = [PSCustomObject] @{page='w-files.pl';proto='https'}
PS> $mojObiekt

page proto
---- -----
w-files.pl https

PS> $przekazaneWartosci.add($mojObiekt)
PS> [string]::isNullOrEmpty($przekazaneWartosci)
True

jeśli obiekt wrzucony do listy jest typu PSCustomObject – nawet jeśli nie jest pusty, funkcja zwraca informację, że lista jest pusta. można by powiedzieć – 'no ale panie, to jest metoda dla stringa, a tutaj masz obiekt, to pewnie dlatego’. tyle, że sprawdzałem na wielu innych typach obiektów – i zachowuje się prawidłowo. póki co ten bug wydaje się działać tylko dla PSCustomObject (-> update dalej). już nawet taka zmiana w deklaracji, z custom na generic:

$myobj = [PSObject] @{page='w-files.pl';proto='https'}

spowoduje, że będzie działać prawidłowo. różnica pomiędzy PSObject a PSCustomObject mogłoby być też fajnym wpisem PS Borderlands i straciłem kiedyś sporo czasu na braq spostrzegawczości, że jest inna deklaracja… ale to nie teraz. dodatkowego smaczq dodaje fakt, że nie do końca wiadomo, kiedy coś zostanie przetworzone na PSCustomObject. ot proszę przykład:

PS> (get-process).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array

PS> (get-process|select processname).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array

PS> $ou='OU=borderlands,DC=w-files,DC=pl'
PS> (Get-ADOrganizationalUnit -Identity $ou).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False ADOrganizationalUnit Microsoft.ActiveDirectory.Management.ADObject

PS C:\CloudTeam-ScriptLAB> (Get-ADOrganizationalUnit -Identity $ou|select name).gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object

z jakiegoś powodu filtrowanie niektórych typów obiektów zmienia ich typ – kolejna zagadka, już poza granicami zrozumienia, ponieważ to nie jest normalne zachowanie.

update

dopiero co skończyłem ten wpis i zabrałem się za kolejny skrypt. kolejny przykład obiektu, dla którego [string]::isNullOrEmpty zwraca głupotę – i to dużo prostszy. wiedziałem, że trafiałem na więcej takich przypadqw!

PS> $storageAccountList = get-AzStorageAccount -resourceGroupName 'myRG'
PS> $storageAccountList

StorageAccountName ResourceGroupName PrimaryLocation SkuName Kind AccessTier CreationTime Provisioning State
------------------ ----------------- --------------- ------- ---- ---------- ------------ ------------
nexortestsa myRG westeurope Standard_LRS StorageV2 Hot 07.05.2021 10:56:16 Succeeded

PS> $storageAccountList.GetType()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSStorageAccount System.Object

PS> [string]::IsNullOrEmpty($storageAccountList)
True

podsumowanie

cała informatyka opiera się na determiniźmie [no w każdym razie do czasu pojawienia się AI/ML, które jest w IT odpowiednikiem fizyki kwantowej w fizyce (; ]. brak determinizmu powoduje, że trzeba się nieźle napocić żeby wyłapać błędy i prawidłowo je obsłużyć.

strasznie mnie drażni, kiedy nie potrafię czegoś zrozumieć. takie 'niuanse’ to jakieś górne wartości zasady pareto – ten promil promila całego systemu, na który trzeba poświęcić mnóstwo, mnóstwo czasu, żeby w ogóle go uchwycić. nie wspominając o zrozumieniu. w tym przypadq – to jest kilka brzegowych sytuacji, które nakładają się na siebie. istna qmulacja. IMHO to nie jest coś 'do zrozumienia’ a bardziej błędy w implementacji, ale póki co – wpada do teczki w-Files.

eN.

 

jak pobrać wartość zmiennej z wyższego zakresu

praktyka

zmienne mają swoje zakresy (scopes). zakresy są niby bardzo proste… ale czy na pewno? to jak pobrać zmienną z zakresu nadrzędnego (parent scope)?

bardzo proste:

get-variable -name <varName> -scope 1

teoria

ale, że '1′? zawsze i niezmiennie '1′? jak to tak? a jak pobrać wartość zakresu w którym obecnie się znajduję?


zmienne układane są na stosie, zwanym czasem kolejką LIFO (Last-In, First-Out). [najstarsza zarejestrowana implementacja LIFO to ’pierwsi będą ostatnimi, a ostatni pierwszymi’ więc łatwo zapamiętać (; ]. element na szczycie stosu, to element 'obecny’, i zawsze ma wartość '0′. proste, ale konsekwencje są nieoczywiste. do tego dochodzi specyfika samego PS i tego, jaki próbuje być 'przyjazny’, co dodatkowo może mieć niespodziewane konsekwencje. oprócz zakresów 'numerowanych’ są zakresy 'nazwane’ i to one powodują sporo zamieszania. nie będę kopiował definicji, które można znaleźć łatwo i szybko, ale z nazwy, podstawowe zakresy to 'Global’ i 'Script’. tak więc uruchamiając dowolny skrypt, zakresy wyglądają tak:

prosty kod testowy:

function level1 {
    $x = 1
    $x
}
$x = 0
level1
$x

wyświetli oczywiście '1′ a następnie '0′ – ponieważ przy każdym wyświetleniu $x wskazuje na zmienną z obecnego zakresu. obecnego, czyli '0′, co obrazuje:

mamy więc odpowiedź 'jak pobrać obecny zakres’ – jest to po prostu zakres zero. skompliqjmy ten skrypt tak, aby przetestować czy faktycznie 'parent scope’ to '1′:

function level1 {
    $x = 1
    "here: $x"
    "parent from l1: {0}" -f (Get-Variable -Name x -Scope 1).Value
    level2
}

function level2 {
    $x = 2
    "here: $x"
    "parent to l2: {0}" -f (get-variable -name x -Scope 1).value
    "script from l2: {0}" -f ($script:x)
}

$x=0
Write-Host -ForegroundColor Magenta "level1:"
level1

Write-Host -ForegroundColor Red "level2:"
level2

write-host -ForegroundColor Green "script:"
$script:x

write-host -ForegroundColor Yellow 'global:'
$Global:x

stos zmiennych dla tego wywołania ma teraz taką strukturę:

oczywiste? ruchome wartości dla zakresów nazwanych mogą sprawić problem i wydawać się nieintuicyjne. ale to nie koniec pułapek. ostatnia wartość [$global:x] może przyjąć albo $null albo… zero. zależnie od środowiska uruchomieniowego [możesz odpalić kod w VSC]. dodatkowo problemy może sprawić zmiana wartości zmiennej nadrzędnej… o ile odczytana zostanie zmienna z dowolnego, pierwszego zakresu, w którym istnieje, o tyle zmiana wartości będzie wykonana w tym samym zakresie. żeby to zobrazować, zmodyfiqjmy odrobinę pierwszy kod:

function level1 {
    $x
    $x = 1
}

$x = 0
level1
$x

tym razem na ekranie pojawią się dwa zera. w funkcji 'level1′ żądamy wyświetlenia $x, które w tym zakresie jeszcze nie istnienie. w związq z tym wyświetlony został $x z zakresu wyżej. ale w momencie wykonania „$x = 1”, zgodnie z definicją, iż przypisanie następuje zawsze w tym samym zakresie, zmienna została zainicjowana lokalnie – a nie nadpisana pierwsza istniejąca – i to jej przypisana została wartość '1′. a więc $x wyświtlony na końcu nie uległ zmianie i wyświelone zostało '0 0′

jeśli chcemy zmienić wartość zmiennej w zakresie nadrzędnym (parent scope) kod wygląda tak:

function level1 {
    $x
    set-variable -name x -scope 1 -Value 1
}

$x = 0
level1
$x

…no i warto zwrócić uwagę, że podając nazwy zmiennych dla commandletów *-variable nie dodaje się '$’.

…i może jeszcze to, że nie ma standardowo metody aby odpytać się o to, jak głęboki jest obecnie stos. jeśli masz taki scenariusz, to przyda ci się jeden z poprzednich wpisów. no dobra, podpowiem… to będzie „((get-PSCallStack).count-1)”.

eN.

AzPseudoGUI – łatwiejsza praca z commandletami Az

trochę GUI dla commandline’a

pracując z linii poleceń braqje możliwości szybkiej weryfikacji nazw i list. ponieważ nie ma GUI, co chwile trzeba listować obiekty, wybierać odpowiednie właściwości (properites) i dopiero w ten sposób wykorzystać nazwę dalej. jest to denerwujące zwłaszcza w połowie pisania jakiegoś długiego oneliner’a i nagle 'o rety, ale jak była nazwa tej resorsgrupy?’ … i ctrl+c, listing wszystkich Resource Group i dopiero od nowa pisanie komendy…

żeby sobie ułatwić życie, oraz pisanie prostych skryptów, przydatny jest moduł AzPseugdoGUI, który obudowuje popularne funkcje Az i dodaje do niech out-gridView. w ten sposób, bez konieczności pamiętania nazw, często dość długich i pokrętnych można np.:

$ctx=select-subscription

co pozwoli wybrać konkretną subskrypcję i zwróci obecny kontext pracy, który można potem wykorzystać.

jak szybko wybrać maszynę wirtualną? po prostu

$vm=select-VM

za dużo maszyn w subskrypcji ale pamiętasz o którą resource group chodzi?

$vm=select-RG -isCritical | select-VM

ułatwia życie i nie trzeba ciągle sięgać do Azure Portal czy listować w kółko… moduł można łatwo zainstalować

install-module AzPseudoGUI

nowy destroy-AzureVM

wykorzystując ten moduł, poprawiłem destroy-AzureVM – który teraz również wykrywa backup maszyny i pozwala wybrać które zasoby mają zostać usunięte a które nie.

przy okazji ciekawostka:

  • W Azure (niemal) wszystko jest zasobem (resource) a dowolny zasób można usunąć uniwersalną funkcją 'remove-AzResource’.

to bardzo wygodnie i nawet złożone konstrukty takie, jak VM można potencjalnie usunąć jedną komendą… niestety zasobem (resource) nie jest kontener Storage Account. ale i tak można zoptymalizować skrypt, tworząc tabelę URI zasobów i wrzucając je do niszczarki:

foreach($resource in $resourcesForDeletion) {
    write-log "removing $resource" -type info
    $resourceSplited=$resource.split('/')
    try{
        remove-AzResource -ResourceID $resource -force
        write-log "$($resourceSplited[-2]): $($resourceSplited[-1]) removed." -type ok
    } catch { 
        write-log "error removing $($resourceSplited[-2]): $($resourceSplited[-1])." -type error
        write-log $_.exception -type error
    }
}
wygodne.
eN.

nazwa skryptu, moduły i stos wywołania

Zakres

robiąc poprzedni wpis stwierdziłem, że największym problemem było pobranie nazwy skryptu uruchamiającego i spodziewałem się, że już jest to ogarnięte. chwilę potem znalazłem dodatkowe scenariusze udowadniające, że sprawa jest jeszcze trudniejsza i bardziej pogmatwana.

temat wydaje się prosty: uruchamiamy skrypt 'skrypt.ps1′ i chcemy założyć plik logu '_skrypt.log’. przy normalnym wywołaniu nie ma najmniejszego problemu. do wyboru mamy kilka zmiennych automatycznych. główną taką zmienną jest $PSScript oraz linqjące do poszczególnych gałęzi skróty:

 

  • $PSCmdlet
  • $MyInvocation ($PSCmdlet.MyInvocation)
  • $PSScriptRoot
  • $PSCommandPath ($MyInvocation.MyCommand.Path)

hint: aby $PSCmdlet zainicjowało się prawidłowo, należy włączyć advanced functions np. przy pomocy cmdletbinding().

w czym zatem problem? otóż $PScmdlet jest inicjowany w momencie ładowania skryptu, i jest zmienną lokalną. mamy więc wiele scenariuszy, w których zamienna przyjmie różne wartości:

  • uruchomione bezpośrednio z linii poleceń (nie inicjuje zmiennych automatycznych, ale to co uruchamiamy zwróci wartość)
  • uruchomione z funkcji, zdefiniowanej w ramach tego samego pliq
  • uruchomione z funkcji, która ładuję bibliotekę
  • uruchomione z funkcji która uruchamia funkcję z innej biblioteki

aby to unaocznić przygotowałem scenariusz testowy – dostępny na GH, jeśli ktoś chciałby się pobawić samemu. jest tu dużo ciekawych niuansów i zabawy (np. niedostępność wartości $MyInvocation podczas debugowania).

hint: jeśli odpalisz plik test-context.ps1 z tego samego katalogu gdzie są moduły testowe, nie musisz ich instalować.

czy zatem nie da się wyciągnąć informacji o faktycznym źródle wywołania?

get-PSCallStack

da się – przy pomocy funkcji get-PSCallStack. Funkcja ta zwraca … właśnie 'call stack’ czyli stos wywołania. przypatrzmy się, co konkretnie zwraca funkcja, zależnie od kontextu uruchomienia

  • bezpośrednio z linii poleceń:
C:\_ScriptZ :))o- Get-PSCallStack

Command Arguments Location
------- --------- --------
<ScriptBlock> {} <No file>
  • z linii poleceń, wywołanie funkcji z modułu
C:\_ScriptZ :))o- level1-callstack

Command Arguments Location
------- --------- --------
level1-callstack {} level1module.psm1: line 18
<ScriptBlock> {} <No file>
  • ze skryptu:
C:\...PSCmdletTest :))o- .\test-contexts.ps1
WARNING: The names of some imported commands from the module 'level1module' include unapproved verbs that might make them less discoverable. To find the
commands with unapproved verbs, run the Import-Module command again with the Verbose parameter. For a list of approved verbs, type Get-Verb.
PSCallStack in different contexts

directly from here

Command Arguments Location
------- --------- --------
test-contexts.ps1 {} test-contexts.ps1: line 8
<ScriptBlock> {} <No file>


from the function loaded by module

Command Arguments Location
------- --------- --------
level1-callstack {} level1module.psm1: line 18
test-contexts.ps1 {} test-contexts.ps1: line 11
<ScriptBlock> {} <No file>


from the function in module L2 calling function in module L1

Command Arguments Location
------- --------- --------
level1-callstack {} level1module.psm1: line 18
level2-callstack {} level2module.psm1: line 11
test-contexts.ps1 {} test-contexts.ps1: line 13
<ScriptBlock> {} <No file>


MtInvocation test from different contexts

first in-script PScommandPath (from myinvocation)
local myinvocation -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\test-contexts.ps1
now PScommandpath but run from the function, loaded in a module L1 ($myinvocation.command.path is empty for functions)
f1 PScommandPath -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1
similar, but function inside module L1 calls function in the same module
f1 via f2 PScommandPath -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1
more complex - call function in module L2, which is calling function in module L1
f1 via diferent module -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1

to co się jako pierwsze rzuca w oczy, równocześnie spójne z definicją 'stosu wykonania’, to że wielkość stosu jest równa zagnieżdżeniu wywołania. z linii poleceń – jest tylko jeden element, bezpośrednio ze skryptu: 2, z funkcji w skrypcie: 3, a z funkcji wołającej funkcję: 4.

drugim elementem rzucającym się w oczy jest kolumna 'location’ która pokazuje ścieżkę do pliq w którym nastąpiło wywołanie, wraz z linią. jest to stos, ale żeby nie było za łatwo – odwrócony. kolejność elementów jest zawsze taka sama ale przez odwrócenie, trzeba zrobić trochę matematyki:

  • zero (na dnie): ostatnie wywołanie
  • ostatnie-1 : właściwy skrypt wywołujący
  • ostatnie: wywołanie bezpośrednie, linia hosta

i te dwie informacje dają nam już możliwość weryfikacji jak dana funkcja jest wywoływana (bezpośrednio z linii poleceń czy gdzieś ze skryptu) oraz dokładne sprawdzenie czy był to moduł, który, a może po prostu jakaś funkcja.

podsumowanie

funkcja get-pscallstack nie jest specjalnie popularna więc nie łatwo było na nią trafić. jednak przy pisaniu modułów lub jakiejś logiki wymagającej wiedzy o środowisq uruchomienia – okazuje się być dużo bardziej przydatna, niż niekontrolowalne $myInvocation, które zmieniają kontext i wartości.

eN.