diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3c2f59 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +# .gitignore diff --git a/AppDeployToolkit/AppDeployToolkitConfig.xml b/AppDeployToolkit/AppDeployToolkitConfig.xml index e0aa7f9..55f2c87 100644 --- a/AppDeployToolkit/AppDeployToolkitConfig.xml +++ b/AppDeployToolkit/AppDeployToolkitConfig.xml @@ -1,4 +1,4 @@ - + - 3.8.2 - 08/05/2020 + 3.8.4 + 26/01/2021 @@ -18,10 +18,16 @@ $envTemp - HKLM:SOFTWARE + HKLM:\SOFTWARE $envWinDir\Logs\Software + $envTemp + + HKCU:\SOFTWARE + + $envProgramData\Logs\Software + False CMTrace @@ -52,6 +58,8 @@ $envWinDir\Logs\Software + $envProgramData\Logs\Software + REBOOT=ReallySuppress /QB-! REBOOT=ReallySuppress /QN @@ -126,8 +134,7 @@ The following programs must be closed before the installation can proceed. Please save your work, close the programs, and then continue. - Alternatively, save your work and click "Close Programs". - + Alternatively, save your work and click "Close Programs". NOTE: The program(s) will be automatically closed in: @@ -181,7 +188,7 @@ Your computer will be automatically restarted at the end of the countdown. Time remaining: - Restart Later + Minimize Restart Now @@ -201,8 +208,7 @@ Følgende programmer skal lukkes før installationen kan fortsætte. Gem dit arbejde, luk programmerne og fortsæt. - Alternativt kan du gemme dit arbejde og trykke på "Luk Programmer". - + Alternativt kan du gemme dit arbejde og trykke på "Luk Programmer". BEMÆRK: Programmet/Programmerne vil automatisk blive lukket om: Følgende applikation vil nu blive installeret: Du kan vælge at udsætte installationen indtil udsættelsesperioden udløber: @@ -231,7 +237,7 @@ Du bør venligst gemme dit arbejde og genstarte indenfor det givne tidsrum. Din computer vil automatisk blive genstartet når nedtællingen er færdig. Tid tilbage: - Genstart Senere + Minimere Genstart Nu @@ -251,8 +257,7 @@ Les programmes suivants doivent être fermés afin que l'installation s'initialise. Merci de sauvegarder votre travail, fermer tous les programmes, et continuer. - Vous pouvez aussi sauvegarder votre travail puis cliquez sur « Fermer Programmes ». - + Vous pouvez aussi sauvegarder votre travail puis cliquez sur « Fermer Programmes ». REMARQUE: Les programmes seront automatiquement fermés dans: L'application suivante est sur le point d'être installée: Vous pouvez choisir de reporter l'installation: @@ -280,7 +285,7 @@ Merci de sauvegarder votre travail et de redémarrer avant que le temps spécifié ne soit écoulé. Votre ordinateur sera automatiquement redémarré à la fin du décompte. Temps restant: - Redémarrer Plus Tard + Minimiser Redémarrer Maintenant @@ -300,8 +305,7 @@ Die folgenden Programme müssen geschlossen werden, bevor die Installation fortgesetzt werden kann. Bitte speichern Sie Ihre Arbeit, schließen Sie die Programme und fahren Sie dann fort. - Alternativ können Sie Ihre Arbeit speichern und dann auf „Programme Schließen“ klicken. - + Alternativ können Sie Ihre Arbeit speichern und dann auf „Programme Schließen“ klicken. HINWEIS: Diese Programme werden automatisch geschlossen: Die folgende Anwendung soll installiert werden: Sie können die Installation verzögern, bis die Rückstellung abläuft: @@ -329,7 +333,7 @@ Bitte speichern Sie Ihre Arbeit und starten Sie den Computer innerhalb der vorgegebenen Zeit neu. Am Ende des Countdowns wird Ihr Computer automatisch neu gestartet. Verbleibende Zeit: - Später Neustarten + Minimieren Jetzt Neustarten @@ -349,8 +353,7 @@ I seguenti programmi devono essere chiusi prima che l'installazione possa procedere. Salvare il lavoro , chiudere i programmi, e poi continuare. - In alternativa, salvare il lavoro e fare clic su "Chiudi Programmi". - + In alternativa, salvare il lavoro e fare clic su "Chiudi Programmi". NOTA: il programma(s) sarà chiuso automaticamente in: La seguente applicazione sta per essere installata: Si può decidere di posticipare l'installazione fino alla prossima richiesta automatica: @@ -378,7 +381,7 @@ Salvare il lavoro e riavviare entro il tempo assegnato. Il computer verrà riavviato automaticamente al termine del conto alla rovescia. Tempo rimanente: - Riavvia Seguito + Minimizzare Riavvia Ora @@ -398,8 +401,7 @@ インストールを実行するために、下記のプログラムを閉じる必要があります。 実行中のアプリケーションを保存し、閉じてから続行してください。 - または、実行中のアプリケーションを保存し、プログラムを強制終了ボタンをクリックしてくだい - + または、実行中のアプリケーションを保存し、プログラムを強制終了ボタンをクリックしてくだい 注意: これらのプログラムは自動的に閉じられます: このアプリケーションはこれからインストールされます。 再試行可能回数が0になるまでは、都合の良い時にインストール可能です。 @@ -427,7 +429,7 @@ 実行中のアプリケーションを保存し、再起動してください。 カウントダウン後にコンピュータが再起動します。 残時間: - 後で再起動 + 最小 化 今すぐ再起動 @@ -447,8 +449,7 @@ Følgende programmer må lukkes før installasjonen kan fortsette. Lagre arbeidet, lukk programmene og velg "Fortsett" - Eller velg "Lukk Programmer" uten å lagre. - + Eller velg "Lukk Programmer" uten å lagre. OBS: Programmet vil automatisk lukkes om: Følgende program vil bli installert: Du kan velge å utsette installasjonen et begrenset antall ganger inntil fristen utløper: @@ -476,7 +477,7 @@ Lagre arbeidet ditt og gjør en omstart av pc innen fristen. Pcen vil automatisk starte på nytt, når nedtellingen er slutt. Tid som gjenstår: - Omstart Senere + Minimere Omstart Nå @@ -496,8 +497,7 @@ De volgende applicaties moeten afgesloten worden om de installatie te voltooien. Sla je werk op, sluit de applicaties, en ga verder. - Of, sla je werk op en klik op 'Sluit Applicaties'. - + Of, sla je werk op en klik op 'Sluit Applicaties'. LET OP: De applicatie(s) worden afgesloten over: De volgende applicatie wordt zometeen geïnstalleerd: Je kan de installatie uitstellen tot het maximale uitsteltermijn is verstreken: @@ -525,7 +525,7 @@ Gelieve je werk op te slaan en binnen het toegestane termijn de computer herstarten De computer zal herstarten als de teller op nul staat Resterende tijd: - Herstart Later + Minimaliseren Herstart Nu @@ -545,8 +545,7 @@ Następujące programy muszą zostać zamknięte przed rozpoczęciem instalacji. Proszę zapisać wszystkie dokumenty i zamknąć programy, a następnie kliknąć przycisk „Kontynuuj”. - Alternatywnie zapisz wszystkie dokumenty i kliknij przycisk „Zamknij Programy”. - + Alternatywnie zapisz wszystkie dokumenty i kliknij przycisk „Zamknij Programy”. UWAGA: Programy zostaną automatycznie zamknięte za: Zostanie zainstalowana następująca aplikacja: Instalacja może zostać przełożona na późniejszy termin. @@ -574,7 +573,7 @@ Proszę zapisać wszystkie dokumenty i zrestartować komputer w wyznaczonym czasie. Komputer zostanie automatycznie zrestartowany po upływie wyznaczonego czasu. Pozostały czas do restartu automatycznego: - Restartuj Później + Zminimalizować Restartuj Teraz @@ -594,8 +593,7 @@ Programas de seguir devem ser fechados antes que a instalação possa prosseguir. Por favor, guarde o seu trabalho, feche os programas e em seguida continuar. - Como alternativa, salve seu trabalho e clique em "Fechar Programas". - + Como alternativa, salve seu trabalho e clique em "Fechar Programas". NOTA: O programa será fechado automaticamente em: O seguinte aplicativo está prestes a ser instalado: Você pode optar por adiar a instalação até que expire o diferimento: @@ -623,7 +621,7 @@ Por favor, salve o trabalho e reiniciar no tempo alocado. Seu computador será reiniciado automaticamente no final da contagem regressiva. Tempo restante: - Reiniciar Mais Tarde + Minimizar Reinicie Agora @@ -643,8 +641,7 @@ Os seguintes programas precisam ser fechados antes que a instalação possa prosseguir. Salve seu trabalho, feche os programas e depois continue. - Como alternativa, salve seu trabalho e clique em "Fechar Programas". - + Como alternativa, salve seu trabalho e clique em "Fechar Programas". OBSERVAÇÃO: O(s) programa(s) será(ão) automaticamente fechado(s) em: O seguinte aplicativo está prestes a ser instalado: Você pode optar por adiar a instalação até que o adiamento expire: @@ -672,7 +669,7 @@ Salve seu trabalho e reinicie dentro do prazo estipulado. Seu computador será reiniciado automaticamente no final da contagem regressiva. Tempo restante: - Reiniciar Mais Tarde + Minimizar Reiniciar Agora @@ -692,8 +689,7 @@ Los siguientes programas deben cerrarse antes de la instalación puede proceder. Por favor, guarde el trabajo, cerrar los programas y luego continuar. - Alternativamente, guarde su trabajo y haga clic en "Cerrar Programas". - + Alternativamente, guarde su trabajo y haga clic en "Cerrar Programas". NOTA: El programa se cerrará automáticamente en: La siguiente aplicación está a punto de instalarse: Puede decidir aplazar la instalación hasta que expire el aplazamiento: @@ -721,7 +717,7 @@ Por favor guarde su trabajo y reinicie dentro del tiempo asignado. El ordenador se reiniciará automáticamente al final de la cuenta regresiva de. Tiempo restante: - Reiniciar Más Tarde + Minimizar Reiniciar Ahora @@ -741,8 +737,7 @@ Följande program måste stängas innan installationen kan fortsätta. Se till att spara ditt arbete, stäng de öppna programmen och klicka sen på "Fortsätt". - Alternativt, spara ditt arbete och klicka på "Stäng Program". - + Alternativt, spara ditt arbete och klicka på "Stäng Program". OBS: Programmen kommer automatiskt att avslutas om: Följande applikation kommer att installeras: Du kan välja att fördröja installationen ett begränsat antal gånger under en begränsad tid: @@ -770,7 +765,7 @@ Se till att spara ditt arbete innan tiden går ut och en automatisk omstart sker. Din dator kommer automatiskt att starta om när nedräkningen är slut. Återstående tid: - Starta Om Senare + Minimera Starta Om Nu @@ -790,8 +785,7 @@ يجب إغلاق البرامج التالية قبل التمكن من متابعة عملية التثبيت. يرجى حفظ عملك، وإغلاق البرامج، ومن ثم المتابعة. - يمكنك بدلا من ذلك، حفظ عملك والنقر فوق "إغلاق البرامج". - + يمكنك بدلا من ذلك، حفظ عملك والنقر فوق "إغلاق البرامج". ملاحظة: سيتم إغلاق البرنامج/البرامج بشكل تلقائي خلال: التطبيق التالي على وشك التثبيت: بإمكانك اختيار تأجيل التثبيت إلى حين انتهاء صلاحية التأجيل: @@ -819,7 +813,7 @@ يرجى حفظ عملك وإعادة التشغيل خلال الوقت المخصص. ستتم إعادة تشغيل حاسوبك بشكل تلقائي عند نهاية العد التنازلي. الزمن المتبقي: - إعادة التشغيل لاحقًا + تقليل إعادة التشغيل الآن @@ -839,8 +833,7 @@ יש לסגור את התכנות הבאות בטרם ההתקנה תוכל להתחיל. אנא שמור על העבודה שלך, סגור את התכניות, ואז המשך. - לחילופין, שמור על העבודה שלך והקלק על "סגור תכניות". - + לחילופין, שמור על העבודה שלך והקלק על "סגור תכניות". שים לב: התכנית(ות) תסגרנה באופן אוטומטי תוך: היישום הבא עומד להיות מותקן: אתה יכול לבחור לדחות את ההתקנה עד שמשך זמן הדחיה יפוג. @@ -868,7 +861,7 @@ אנא שמור על העבודה שלך ואתחל במסגרת הזמן המוקצב. המחשב שלך יאותחל באופן אוטומטי בסיום הספירה לאחור. הזמן הנותר: - אתחל מאוחר יותר + מזער את אתחל עכשיו @@ -888,8 +881,7 @@ 설치를 계속하려면 다음의 프로그램을 종료해야 합니다. 사용자 작업을 저장하고 프로그램을 종료한 후 계속하세요. - 다른 방법으로는 사용자 작업을 저장하고 "프로그램 종료"를 클릭하세요. - + 다른 방법으로는 사용자 작업을 저장하고 "프로그램 종료"를 클릭하세요. 참고: 프로그램이 자동으로 종료되는 경우: 다음의 응용 프로그램을 설치합니다: 지연 기간이 만료될 때까지 설치를 연기할 수 있습니다: @@ -917,7 +909,7 @@ 사용자 작업을 저장하고 지정된 시간 이내에 다시 시작하세요. 카운트다운이 종료되면 컴퓨터는 자동으로 다시 시작합니다. 남은 시간: - 나중에 다시 시작 + 최소화 지금 다시 시작 @@ -937,8 +929,7 @@ Перед продолжением установки необходимо закрыть следующие программы. Пожалуйста, сохраните вашу работу и закройте программы, а затем продолжите установку. - Также вы можете сохранить вашу работу и нажать "Закрыть программы". - + Также вы можете сохранить вашу работу и нажать "Закрыть программы". ПРИМЕЧАНИЕ: Эти программы будут автоматически закрыты через: Планируется установка следующего приложения: Вы можете отложить установку приложения до тех пор, пока не истечет срок действия этой отсрочки: @@ -966,7 +957,7 @@ Пожалуйста, сохраните вашу работу и выполните перезагрузку в отведенное время. Ваш компьютер будет автоматически перезагружен по завершению обратного отсчета. Оставшееся время: - Перезагрузить позже + Минимизировать Перезагрузить сейчас @@ -986,8 +977,7 @@ 为继续安装,必须关闭下列程序。 请保存您的工作,关闭程序,然后继续。 - 或者保存您的工作,点击"关闭程序"。 - + 或者保存您的工作,点击"关闭程序"。 注:下列程序将自动关闭: 即将安装下列应用程式: 在延期失效前,可选择延迟安装: @@ -1015,7 +1005,7 @@ 请保存您的工作,并在容许时间重启计算机。 倒计时结束后,计算机将自动重启。 剩余时间: - 稍后重启 + 最小化 现在重启 @@ -1035,8 +1025,7 @@ 在繼續安裝前必須關閉下列程序。 請保存您的工作,關閉程序,然後繼續。 - 或者保存您的工作,然後點擊"關閉程序"。 - + 或者保存您的工作,然後點擊"關閉程序"。 注:下列程序將自動關閉: 即將安裝下列應用程式: 在延期失效前,可選擇延遲安裝: @@ -1064,7 +1053,7 @@ 請保存您的工作,然後在容許時間重啟計算機。 倒計時結束後,計算機將自動重啟。 剩餘時間: - 稍後重啟 + 最小化 現在重啟 @@ -1084,8 +1073,7 @@ Nasledujúce programy musia byť zatvorené, než bude inštalácia pokračovať. Prosím, uložte svoju prácu, zatvorte dané programy a potom kliknite na pokračovať. - Prípadne môžete uložiť svoju prácu a potom kliknite na tlačidlo "Ukončiť programy". - + Prípadne môžete uložiť svoju prácu a potom kliknite na tlačidlo "Ukončiť programy". Poznámka: Programy budú automaticky ukončené za: Nasledujúca aplikácia bude nainštalovaná: Inštaláciu môžete niekoľkokrát odložiť: @@ -1113,7 +1101,7 @@ Prosím, uložte si prácu a reštartujte počítač v stanovenej lehote. Na konci odpočítavania, bude váš počítač automaticky reštartovaný. Zostávajúci čas: - Reštartovať Neskôr + Minimalizovať Reštartovať Teraz @@ -1133,8 +1121,7 @@ Následující programy musí být zavřené, aby instalace mohla pokračovat. Prosím, uložte svou práci, zavřete program a potom klikněte na "Pokračovat". - Případně můžete svou práci uložit a kliknout na tlačítko "Ukončit programy". - + Případně můžete svou práci uložit a kliknout na tlačítko "Ukončit programy". Upozornění: Programy budou automaticky zavřené za: Nasledující aplikace bude nainstalována: Instalaci můžete několikrát odložit: @@ -1162,7 +1149,7 @@ Prosím, uložte si práci a restartujte počítač ve stanoveném čase. Na konci odpočítávání, bude váš počítač automaticky restartovaný. Zbývající čas: - Restartovat později + Minimalizovat Restartovat nyní @@ -1181,8 +1168,7 @@ Kérjük mentse munkáját és a folytatáshoz zárja be a futó alkalmazásokat. Vagy - Kérjük mentse munkáját és kattintson a „Programok bezárása”-ra. - + Kérjük mentse munkáját és kattintson a „Programok bezárása”-ra. Megjegyzés: a programok automatikusan bezárásra kerülnek,: A következő alkalmazások telepítésre kerülnek: A telepítést elhalaszthatja amíg a rendelkezésre álló idő lejár: @@ -1210,7 +1196,7 @@ Kérem mentse munkáját, és a megadott időn belül indítsa újra.. A hátralévő idő leteltével a számítógép újraindul. Hátralévő idő: - Újraindítás később + Minimalizál Újraindítás most diff --git a/AppDeployToolkit/AppDeployToolkitExtensions.ps1 b/AppDeployToolkit/AppDeployToolkitExtensions.ps1 index 6bbe8a4..7066eb1 100644 --- a/AppDeployToolkit/AppDeployToolkitExtensions.ps1 +++ b/AppDeployToolkit/AppDeployToolkitExtensions.ps1 @@ -27,8 +27,8 @@ Param ( # Variables: Script [string]$appDeployToolkitExtName = 'PSAppDeployToolkitExt' [string]$appDeployExtScriptFriendlyName = 'App Deploy Toolkit Extensions' -[version]$appDeployExtScriptVersion = [version]'3.8.2' -[string]$appDeployExtScriptDate = '08/05/2020' +[version]$appDeployExtScriptVersion = [version]'3.8.4' +[string]$appDeployExtScriptDate = '26/01/2021' [hashtable]$appDeployExtScriptParameters = $PSBoundParameters ##*=============================================== diff --git a/AppDeployToolkit/AppDeployToolkitHelp.ps1 b/AppDeployToolkit/AppDeployToolkitHelp.ps1 index dc516fb..313936a 100644 --- a/AppDeployToolkit/AppDeployToolkitHelp.ps1 +++ b/AppDeployToolkit/AppDeployToolkitHelp.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Displays a graphical console to browse the help for the App Deployment Toolkit functions # LICENSE # @@ -22,8 +22,8 @@ ## Variables: Script [string]$appDeployToolkitHelpName = 'PSAppDeployToolkitHelp' [string]$appDeployHelpScriptFriendlyName = 'App Deploy Toolkit Help' -[version]$appDeployHelpScriptVersion = [version]'3.8.2' -[string]$appDeployHelpScriptDate = '08/05/2020' +[version]$appDeployHelpScriptVersion = [version]'3.8.4' +[string]$appDeployHelpScriptDate = '26/01/2021' ## Variables: Environment [string]$scriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent diff --git a/AppDeployToolkit/AppDeployToolkitMain.cs b/AppDeployToolkit/AppDeployToolkitMain.cs index 2e952ce..1995b3a 100644 --- a/AppDeployToolkit/AppDeployToolkitMain.cs +++ b/AppDeployToolkit/AppDeployToolkitMain.cs @@ -1,5 +1,5 @@ -// Date Modified: 08/05/2020 -// Version Number: 3.8.2 +// Date Modified: 26/01/2021 +// Version Number: 3.8.4 using System; using System.Text; @@ -30,7 +30,7 @@ enum LoadLibraryFlags : int static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, LoadLibraryFlags dwFlags); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - static extern int LoadString(IntPtr hInstance, int uID, StringBuilder lpBuffer, int nBufferMax); + static extern int LoadString(IntPtr hInstance, uint uID, StringBuilder lpBuffer, int nBufferMax); // Get MSI exit code message from msimsg.dll resource dll public static string GetMessageFromMsiExitCode(int errCode) @@ -38,7 +38,8 @@ public static string GetMessageFromMsiExitCode(int errCode) IntPtr hModuleInstance = LoadLibraryEx("msimsg.dll", IntPtr.Zero, LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE); StringBuilder sb = new StringBuilder(255); - LoadString(hModuleInstance, errCode, sb, sb.Capacity + 1); + uint u = Convert.ToUInt32(errCode); + LoadString(hModuleInstance, u, sb, sb.Capacity + 1); return sb.ToString(); } @@ -54,10 +55,10 @@ public class Explorer static extern bool SendNotifyMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - private static extern IntPtr SendMessageTimeout(IntPtr hWnd, int Msg, IntPtr wParam, string lParam, int fuFlags, int uTimeout, IntPtr lpdwResult); + private static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, IntPtr wParam, string lParam, uint fuFlags, uint uTimeout, IntPtr lpdwResult); [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = false)] - private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2); + private static extern void SHChangeNotify(long eventId, uint flags, IntPtr item1, IntPtr item2); public static void RefreshDesktopAndEnvironmentVariables() { @@ -72,7 +73,7 @@ public static void RefreshDesktopAndEnvironmentVariables() public sealed class FileVerb { [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern int LoadString(IntPtr h, int id, StringBuilder sb, int maxBuffer); + public static extern int LoadString(IntPtr h, uint id, StringBuilder sb, int maxBuffer); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = false)] public static extern IntPtr LoadLibrary(string s); @@ -82,8 +83,8 @@ public static string GetPinVerb(int VerbId) IntPtr hShell32 = LoadLibrary("shell32.dll"); const int nChars = 255; StringBuilder Buff = new StringBuilder("", nChars); - - LoadString(hShell32, VerbId, Buff, Buff.Capacity); + uint u = Convert.ToUInt32(VerbId); + LoadString(hShell32, u, Buff, Buff.Capacity); return Buff.ToString(); } } diff --git a/AppDeployToolkit/AppDeployToolkitMain.ps1 b/AppDeployToolkit/AppDeployToolkitMain.ps1 index a13aa36..79b419c 100644 --- a/AppDeployToolkit/AppDeployToolkitMain.ps1 +++ b/AppDeployToolkit/AppDeployToolkitMain.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS This script contains the functions and logic engine for the Deploy-Application.ps1 script. # LICENSE # @@ -70,9 +70,9 @@ Param ( [string]$appDeployMainScriptFriendlyName = 'App Deploy Toolkit Main' ## Variables: Script Info -[version]$appDeployMainScriptVersion = [version]'3.8.2' -[version]$appDeployMainScriptMinimumConfigVersion = [version]'3.8.2' -[string]$appDeployMainScriptDate = '08/05/2020' +[version]$appDeployMainScriptVersion = [version]'3.8.4' +[version]$appDeployMainScriptMinimumConfigVersion = [version]'3.8.4' +[string]$appDeployMainScriptDate = '26/01/2021' [hashtable]$appDeployMainScriptParameters = $PSBoundParameters ## Variables: Datetime and Culture @@ -87,27 +87,22 @@ Param ( ## Variables: Environment Variables [psobject]$envHost = $Host -[psobject]$envShellFolders = Get-ItemProperty -Path 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders' -ErrorAction 'SilentlyContinue' +[psobject]$envShellFolders = Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders' -ErrorAction 'SilentlyContinue' [string]$envAllUsersProfile = $env:ALLUSERSPROFILE [string]$envAppData = [Environment]::GetFolderPath('ApplicationData') [string]$envArchitecture = $env:PROCESSOR_ARCHITECTURE -[string]$envCommonProgramFiles = [Environment]::GetFolderPath('CommonProgramFiles') -[string]$envCommonProgramFilesX86 = ${env:CommonProgramFiles(x86)} -[string]$envCommonDesktop = $envShellFolders | Select-Object -ExpandProperty 'Common Desktop' -ErrorAction 'SilentlyContinue' -[string]$envCommonDocuments = $envShellFolders | Select-Object -ExpandProperty 'Common Documents' -ErrorAction 'SilentlyContinue' -[string]$envCommonStartMenuPrograms = $envShellFolders | Select-Object -ExpandProperty 'Common Programs' -ErrorAction 'SilentlyContinue' -[string]$envCommonStartMenu = $envShellFolders | Select-Object -ExpandProperty 'Common Start Menu' -ErrorAction 'SilentlyContinue' -[string]$envCommonStartUp = $envShellFolders | Select-Object -ExpandProperty 'Common Startup' -ErrorAction 'SilentlyContinue' -[string]$envCommonTemplates = $envShellFolders | Select-Object -ExpandProperty 'Common Templates' -ErrorAction 'SilentlyContinue' +[string]$envCommonDesktop = $envShellFolders.'Common Desktop' +[string]$envCommonDocuments = $envShellFolders.'Common Documents' +[string]$envCommonStartMenuPrograms = $envShellFolders.'Common Programs' +[string]$envCommonStartMenu = $envShellFolders.'Common Start Menu' +[string]$envCommonStartUp = $envShellFolders.'Common Startup' +[string]$envCommonTemplates = $envShellFolders.'Common Templates' [string]$envComputerName = [Environment]::MachineName.ToUpper() -[string]$envComputerNameFQDN = ([Net.Dns]::GetHostEntry('localhost')).HostName [string]$envHomeDrive = $env:HOMEDRIVE [string]$envHomePath = $env:HOMEPATH [string]$envHomeShare = $env:HOMESHARE [string]$envLocalAppData = [Environment]::GetFolderPath('LocalApplicationData') [string[]]$envLogicalDrives = [Environment]::GetLogicalDrives() -[string]$envProgramFiles = [Environment]::GetFolderPath('ProgramFiles') -[string]$envProgramFilesX86 = ${env:ProgramFiles(x86)} [string]$envProgramData = [Environment]::GetFolderPath('CommonApplicationData') [string]$envPublic = $env:PUBLIC [string]$envSystemDrive = $env:SYSTEMDRIVE @@ -129,9 +124,6 @@ Param ( [string]$envUserTemplates = [Environment]::GetFolderPath('Templates') [string]$envSystem32Directory = [Environment]::SystemDirectory [string]$envWinDir = $env:WINDIR -# Handle X86 environment variables so they are never empty -If (-not $envCommonProgramFilesX86) { [string]$envCommonProgramFilesX86 = $envCommonProgramFiles } -If (-not $envProgramFilesX86) { [string]$envProgramFilesX86 = $envProgramFiles } ## Variables: Domain Membership [boolean]$IsMachinePartOfDomain = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'SilentlyContinue').PartOfDomain @@ -139,21 +131,38 @@ If (-not $envProgramFilesX86) { [string]$envProgramFilesX86 = $envProgramFiles } [string]$envMachineADDomain = '' [string]$envLogonServer = '' [string]$MachineDomainController = '' +[string]$envComputerNameFQDN = $envComputerName If ($IsMachinePartOfDomain) { - [string]$envMachineADDomain = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'SilentlyContinue').Domain | Where-Object { $_ } | ForEach-Object { $_.ToLower() } + [string]$envMachineADDomain = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'SilentlyContinue').Domain | ForEach-Object { if($_){$_.ToLower()} } + try { + $envComputerNameFQDN = ([Net.Dns]::GetHostEntry('localhost')).HostName + } + catch { + # Function GetHostEntry failed, but we can construct the FQDN in another way + $envComputerNameFQDN = $envComputerNameFQDN + "." + $envMachineADDomain + } + Try { - [string]$envLogonServer = $env:LOGONSERVER | Where-Object { (($_) -and (-not $_.Contains('\\MicrosoftAccount'))) } | ForEach-Object { $_.TrimStart('\') } | ForEach-Object { ([Net.Dns]::GetHostEntry($_)).HostName } - # If running in system context, fall back on the logonserver value stored in the registry - If (-not $envLogonServer) { [string]$envLogonServer = Get-ItemProperty -LiteralPath 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\History' -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty 'DCName' -ErrorAction 'SilentlyContinue' } - [string]$MachineDomainController = [DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().FindDomainController().Name + [string]$envLogonServer = $env:LOGONSERVER | ForEach-Object { if(($_) -and (-not $_.Contains('\\MicrosoftAccount'))) { ([Net.Dns]::GetHostEntry($_.TrimStart('\'))).HostName } } } Catch { } + # If running in system context or if GetHostEntry fails, fall back on the logonserver value stored in the registry + If (-not $envLogonServer) { [string]$envLogonServer = (Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\History' -ErrorAction 'SilentlyContinue').DCName } + ## Remove backslashes at the beginning + while ($envLogonServer.StartsWith('\')) { + $envLogonServer = $envLogonServer.Substring(1) + } + + try { + [string]$MachineDomainController = [DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().FindDomainController().Name + } + catch { } } Else { - [string]$envMachineWorkgroup = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'SilentlyContinue').Domain | Where-Object { $_ } | ForEach-Object { $_.ToUpper() } + [string]$envMachineWorkgroup = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'SilentlyContinue').Domain | ForEach-Object { if($_){$_.ToUpper()} } } -[string]$envMachineDNSDomain = [Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName | Where-Object { $_ } | ForEach-Object { $_.ToLower() } -[string]$envUserDNSDomain = $env:USERDNSDOMAIN | Where-Object { $_ } | ForEach-Object { $_.ToLower() } +[string]$envMachineDNSDomain = [Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName | ForEach-Object { if($_){$_.ToLower()} } +[string]$envUserDNSDomain = $env:USERDNSDOMAIN | ForEach-Object { if($_){$_.ToLower()} } Try { [string]$envUserDomain = [Environment]::UserDomainName.ToUpper() } @@ -167,10 +176,14 @@ Catch { } [string]$envOSVersionMajor = $envOSVersion.Major [string]$envOSVersionMinor = $envOSVersion.Minor [string]$envOSVersionBuild = $envOSVersion.Build -If ($envOSVersionMajor -eq 10) {$envOSVersionRevision = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name 'UBR' -ErrorAction SilentlyContinue).UBR} -Else { [string]$envOSVersionRevision = ,((Get-ItemProperty -Path 'HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name 'BuildLabEx' -ErrorAction 'SilentlyContinue').BuildLabEx -split '\.') | ForEach-Object { $_[1] } } +If ((Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction 'SilentlyContinue').PSObject.Properties.Name -contains 'UBR') { + [string]$envOSVersionRevision = (Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name 'UBR' -ErrorAction 'SilentlyContinue').UBR +} +ElseIf ((Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction 'SilentlyContinue').PSObject.Properties.Name -contains 'BuildLabEx') { + [string]$envOSVersionRevision = ,((Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name 'BuildLabEx' -ErrorAction 'SilentlyContinue').BuildLabEx -split '\.') | ForEach-Object { $_[1] } +} If ($envOSVersionRevision -notmatch '^[\d\.]+$') { $envOSVersionRevision = '' } -If ($envOSVersionRevision) { [string]$envOSVersion = "$($envOSVersion.ToString()).$envOSVersionRevision" } Else { "$($envOSVersion.ToString())" } +If ($envOSVersionRevision) { [string]$envOSVersion = "$($envOSVersion.ToString()).$envOSVersionRevision" } Else { [string]$envOSVersion = "$($envOSVersion.ToString())" } # Get the operating system type [int32]$envOSProductType = $envOS.ProductType [boolean]$IsServerOS = [boolean]($envOSProductType -eq 3) @@ -183,13 +196,44 @@ Switch ($envOSProductType) { Default { [string]$envOSProductTypeName = 'Unknown' } } # Get the OS Architecture -[boolean]$Is64Bit = [boolean]((Get-WmiObject -Class 'Win32_Processor' -ErrorAction 'SilentlyContinue' | Where-Object { $_.DeviceID -eq 'CPU0' } | Select-Object -ExpandProperty 'AddressWidth') -eq 64) +[boolean]$Is64Bit = [boolean]((Get-WmiObject -Class 'Win32_Processor' -ErrorAction 'SilentlyContinue' | ForEach-Object { if($_.DeviceID -eq 'CPU0') { $_.AddressWidth} }) -eq 64) If ($Is64Bit) { [string]$envOSArchitecture = '64-bit' } Else { [string]$envOSArchitecture = '32-bit' } ## Variables: Current Process Architecture [boolean]$Is64BitProcess = [boolean]([IntPtr]::Size -eq 8) If ($Is64BitProcess) { [string]$psArchitecture = 'x64' } Else { [string]$psArchitecture = 'x86' } +## Variables: Get Normalized ProgramFiles and CommonProgramFiles Paths +[string]$envProgramFiles = '' +[string]$envProgramFilesX86 = '' +[string]$envCommonProgramFiles = '' +[string]$envCommonProgramFilesX86 = '' +If ($Is64Bit) { + If ($Is64BitProcess) { + [string]$envProgramFiles = [Environment]::GetFolderPath('ProgramFiles') + [string]$envCommonProgramFiles = [Environment]::GetFolderPath('CommonProgramFiles') + } + Else { + [string]$envProgramFiles = [Environment]::GetEnvironmentVariable('ProgramW6432') + [string]$envCommonProgramFiles = [Environment]::GetEnvironmentVariable('CommonProgramW6432') + } + ## Powershell 2 doesn't support X86 folders so need to use variables instead + try { + [string]$envProgramFilesX86 = [Environment]::GetFolderPath('ProgramFilesX86') + [string]$envCommonProgramFilesX86 = [Environment]::GetFolderPath('CommonProgramFilesX86') + } + catch { + [string]$envProgramFilesX86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)') + [string]$envCommonProgramFilesX86 = [Environment]::GetEnvironmentVariable('CommonProgramFiles(x86)') + } +} +Else { + [string]$envProgramFiles = [Environment]::GetFolderPath('ProgramFiles') + [string]$envProgramFilesX86 = $envProgramFiles + [string]$envCommonProgramFiles = [Environment]::GetFolderPath('CommonProgramFiles') + [string]$envCommonProgramFilesX86 = $envCommonProgramFiles +} + ## Variables: Hardware [int32]$envSystemRAM = Get-WMIObject -Class Win32_PhysicalMemory -ComputerName $env:COMPUTERNAME -ErrorAction 'SilentlyContinue' | Measure-Object -Property Capacity -Sum -ErrorAction SilentlyContinue | ForEach-Object {[Math]::Round(($_.sum / 1GB),2)} @@ -222,6 +266,9 @@ If ($Is64BitProcess) { [string]$psArchitecture = 'x64' } Else { [string]$psArchi [boolean]$IsServiceAccount = [boolean]($CurrentProcessToken.Groups -contains [Security.Principal.SecurityIdentifier]'S-1-5-6') [boolean]$IsProcessUserInteractive = [Environment]::UserInteractive [string]$LocalSystemNTAccount = (New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList ([Security.Principal.WellKnownSidType]::'LocalSystemSid', $null)).Translate([Security.Principal.NTAccount]).Value +[string]$LocalUsersGroup = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList ([Security.Principal.WellKnownSidType]::'BuiltinUsersSid', $null)).Translate([System.Security.Principal.NTAccount]).Value +[string]$LocalPowerUsersGroup = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList ([Security.Principal.WellKnownSidType]::'BuiltinPowerUsersSid', $null)).Translate([System.Security.Principal.NTAccount]).Value +[string]$LocalAdministratorsGroup = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList ([Security.Principal.WellKnownSidType]::'BuiltinAdministratorsSid', $null)).Translate([System.Security.Principal.NTAccount]).Value # Check if script is running in session zero If ($IsLocalSystemAccount -or $IsLocalServiceAccount -or $IsNetworkServiceAccount -or $IsServiceAccount) { $SessionZero = $true } Else { $SessionZero = $false } @@ -251,7 +298,7 @@ If (-not (Test-Path -LiteralPath $appDeployCustomTypesSourceCode -PathType 'Leaf [string]$appDeployToolkitDotSourceExtensions = 'AppDeployToolkitExtensions.ps1' ## Import variables from XML configuration file -[Xml.XmlDocument]$xmlConfigFile = Get-Content -LiteralPath $AppDeployConfigFile +[Xml.XmlDocument]$xmlConfigFile = Get-Content -LiteralPath $AppDeployConfigFile -Encoding UTF8 [Xml.XmlElement]$xmlConfig = $xmlConfigFile.AppDeployToolkit_Config # Get Config File Details [Xml.XmlElement]$configConfigDetails = $xmlConfig.Config_File @@ -270,16 +317,6 @@ If (-not (Test-Path -LiteralPath $appDeployCustomTypesSourceCode -PathType 'Leaf If (-not (Test-Path -LiteralPath $appDeployLogoIcon -PathType 'Leaf')) { Throw 'App Deploy logo icon file not found.' } If (-not (Test-Path -LiteralPath $appDeployLogoBanner -PathType 'Leaf')) { Throw 'App Deploy logo banner file not found.' } -Add-Type -AssemblyName 'System.Drawing' -ErrorAction 'Stop' -[System.Drawing.Bitmap]$appDeployLogoBannerObject = New-Object System.Drawing.Bitmap $appDeployLogoBanner -[Int32]$appDeployLogoBannerBaseHeight = 50 - -[Int32]$appDeployLogoBannerHeight = $appDeployLogoBannerObject.Height -if ($appDeployLogoBannerHeight -gt $appDeployLogoBannerMaxHeight) { - $appDeployLogoBannerHeight = $appDeployLogoBannerMaxHeight -} -[Int32]$appDeployLogoBannerHeightDifference = $appDeployLogoBannerHeight - $appDeployLogoBannerBaseHeight - # Get Toolkit Options [Xml.XmlElement]$xmlToolkitOptions = $xmlConfig.Toolkit_Options [boolean]$configToolkitRequireAdmin = [boolean]::Parse($xmlToolkitOptions.Toolkit_RequireAdmin) @@ -299,6 +336,21 @@ if ($appDeployLogoBannerHeight -gt $appDeployLogoBannerMaxHeight) { [string]$configMSIUninstallParams = $ExecutionContext.InvokeCommand.ExpandString($xmlConfigMSIOptions.MSI_UninstallParams) [string]$configMSILogDir = $ExecutionContext.InvokeCommand.ExpandString($xmlConfigMSIOptions.MSI_LogPath) [int32]$configMSIMutexWaitTime = $xmlConfigMSIOptions.MSI_MutexWaitTime +# Change paths to user accessible ones if RequireAdmin is false +If ($configToolkitRequireAdmin -eq $false){ + If ($xmlToolkitOptions.Toolkit_TempPathNoAdminRights) { + [string]$configToolkitTempPath = $ExecutionContext.InvokeCommand.ExpandString($xmlToolkitOptions.Toolkit_TempPathNoAdminRights) + } + If ($xmlToolkitOptions.Toolkit_RegPathNoAdminRights) { + [string]$configToolkitRegPath = $xmlToolkitOptions.Toolkit_RegPathNoAdminRights + } + If ($xmlToolkitOptions.Toolkit_LogPathNoAdminRights) { + [string]$configToolkitLogDir = $ExecutionContext.InvokeCommand.ExpandString($xmlToolkitOptions.Toolkit_LogPathNoAdminRights) + } + If ($xmlConfigMSIOptions.MSI_LogPathNoAdminRights) { + [string]$configMSILogDir = $ExecutionContext.InvokeCommand.ExpandString($xmlConfigMSIOptions.MSI_LogPathNoAdminRights) + } +} # Get UI Options [Xml.XmlElement]$xmlConfigUIOptions = $xmlConfig.UI_Options [string]$configInstallationUILanguageOverride = $xmlConfigUIOptions.InstallationUI_LanguageOverride @@ -317,24 +369,24 @@ if ($appDeployLogoBannerHeight -gt $appDeployLogoBannerMaxHeight) { If ($RunAsActiveUser) { # Read language defined by Group Policy If (-not $HKULanguages) { - [string[]]$HKULanguages = Get-RegistryKey -Key 'HKLM:SOFTWARE\Policies\Microsoft\MUI\Settings' -Value 'PreferredUILanguages' + [string[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\MUI\Settings' -Value 'PreferredUILanguages' } If (-not $HKULanguages) { - [string[]]$HKULanguages = Get-RegistryKey -Key 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop' -Value 'PreferredUILanguages' -SID $RunAsActiveUser.SID + [string[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\Control Panel\Desktop' -Value 'PreferredUILanguages' -SID $RunAsActiveUser.SID } # Read language for Win Vista & higher machines If (-not $HKULanguages) { - [string[]]$HKULanguages = Get-RegistryKey -Key 'HKCU\Control Panel\Desktop' -Value 'PreferredUILanguages' -SID $RunAsActiveUser.SID + [string[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop' -Value 'PreferredUILanguages' -SID $RunAsActiveUser.SID } If (-not $HKULanguages) { - [string[]]$HKULanguages = Get-RegistryKey -Key 'HKCU\Control Panel\Desktop\MuiCached' -Value 'MachinePreferredUILanguages' -SID $RunAsActiveUser.SID + [string[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop\MuiCached' -Value 'MachinePreferredUILanguages' -SID $RunAsActiveUser.SID } If (-not $HKULanguages) { - [string[]]$HKULanguages = Get-RegistryKey -Key 'HKCU\Control Panel\International' -Value 'LocaleName' -SID $RunAsActiveUser.SID + [string[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\International' -Value 'LocaleName' -SID $RunAsActiveUser.SID } # Read language for Win XP machines If (-not $HKULanguages) { - [string]$HKULocale = Get-RegistryKey -Key 'HKCU\Control Panel\International' -Value 'Locale' -SID $RunAsActiveUser.SID + [string]$HKULocale = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\International' -Value 'Locale' -SID $RunAsActiveUser.SID If ($HKULocale) { [int32]$HKULocale = [Convert]::ToInt32('0x' + $HKULocale, 16) [string[]]$HKULanguages = ([Globalization.CultureInfo]($HKULocale)).Name @@ -421,8 +473,8 @@ if ($appDeployLogoBannerHeight -gt $appDeployLogoBannerMaxHeight) { If (-not $deploymentType) { [string]$deploymentType = 'Install' } ## Variables: Executables -[string]$exeWusa = 'wusa.exe' # Installs Standalone Windows Updates -[string]$exeMsiexec = 'msiexec.exe' # Installs MSI Installers +[string]$exeWusa = "$envWinDir\System32\wusa.exe" # Installs Standalone Windows Updates +[string]$exeMsiexec = "$envWinDir\System32\msiexec.exe" # Installs MSI Installers [string]$exeSchTasks = "$envWinDir\System32\schtasks.exe" # Manages Scheduled Tasks ## Variables: RegEx Patterns @@ -433,14 +485,14 @@ If (-not $deploymentType) { [string]$deploymentType = 'Install' } ## Variables: Registry Keys # Registry keys for native and WOW64 applications -[string[]]$regKeyApplications = 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall','HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' +[string[]]$regKeyApplications = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall','Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' If ($is64Bit) { - [string]$regKeyLotusNotes = 'HKLM:SOFTWARE\Wow6432Node\Lotus\Notes' + [string]$regKeyLotusNotes = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Lotus\Notes' } Else { - [string]$regKeyLotusNotes = 'HKLM:SOFTWARE\Lotus\Notes' + [string]$regKeyLotusNotes = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Lotus\Notes' } -[string]$regKeyAppExecution = 'HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options' +[string]$regKeyAppExecution = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options' ## COM Objects: Initialize [__comobject]$Shell = New-Object -ComObject 'WScript.Shell' -ErrorAction 'SilentlyContinue' @@ -462,15 +514,15 @@ If (Test-Path -LiteralPath 'variable:deferDays') { Remove-Variable -Name 'deferD # If a user is logged on, then get display scale factor for logged on user (even if running in session 0) [boolean]$UserDisplayScaleFactor = $false If ($RunAsActiveUser) { - [int32]$dpiPixels = Get-RegistryKey -Key 'HKCU\Control Panel\Desktop\WindowMetrics' -Value 'AppliedDPI' -SID $RunAsActiveUser.SID + [int32]$dpiPixels = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics' -Value 'AppliedDPI' -SID $RunAsActiveUser.SID If (-not ([string]$dpiPixels)) { - [int32]$dpiPixels = Get-RegistryKey -Key 'HKCU\Control Panel\Desktop' -Value 'LogPixels' -SID $RunAsActiveUser.SID + [int32]$dpiPixels = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop' -Value 'LogPixels' -SID $RunAsActiveUser.SID } [boolean]$UserDisplayScaleFactor = $true } If (-not ([string]$dpiPixels)) { # This registry setting only exists if system scale factor has been changed at least once - [int32]$dpiPixels = Get-RegistryKey -Key 'HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI' -Value 'LogPixels' + [int32]$dpiPixels = Get-RegistryKey -Key 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI' -Value 'LogPixels' [boolean]$UserDisplayScaleFactor = $false } Switch ($dpiPixels) { @@ -553,9 +605,15 @@ Function Execute-MSP { .SYNOPSIS Reads SummaryInfo targeted product codes in MSP file and determines if the MSP file applies to any installed products If a valid installed product is found, triggers the Execute-MSI function to patch the installation. + Uses default config MSI parameters. You can use -AddParameters to add additional parameters. .PARAMETER Path + Path to the msp file +.PARAMETER AddParameters + Additional parameters .EXAMPLE Execute-MSP -Path 'Adobe_Reader_11.0.3_EN.msp' +.EXAMPLE + Execute-MSP -Path 'AcroRdr2017Upd1701130143_MUI.msp' -AddParameters 'ALLUSERS=1' .NOTES .LINK http://psappdeploytoolkit.com @@ -565,7 +623,10 @@ Function Execute-MSP { [Parameter(Mandatory=$true,HelpMessage='Please enter the path to the MSP file')] [ValidateScript({('.msp' -contains [IO.Path]::GetExtension($_))})] [Alias('FilePath')] - [string]$Path + [string]$Path, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$AddParameters ) Begin { @@ -604,7 +665,14 @@ Function Execute-MSP { Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($SummaryInformation) } Catch { } Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($DataBase) } Catch { } Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Installer) } Catch { } - If ($IsMSPNeeded) { Execute-MSI -Action Patch -Path $Path } + If ($IsMSPNeeded) { + If ($AddParameters) { + Execute-MSI -Action Patch -Path $Path -AddParameters $AddParameters + } + Else { + Execute-MSI -Action Patch -Path $Path + } + } } } #endregion @@ -647,6 +715,8 @@ Function Write-Log { Write-Log -Message "Installing patch MS15-031" -Source 'Add-Patch' -LogType 'CMTrace' .EXAMPLE Write-Log -Message "Script is running on Windows 8" -Source 'Test-ValidOS' -LogType 'Legacy' +.EXAMPLE + Write-Log -Message "Log only message" -WriteHost $false .NOTES .LINK http://psappdeploytoolkit.com @@ -662,7 +732,7 @@ Function Write-Log { [int16]$Severity = 1, [Parameter(Mandatory=$false,Position=2)] [ValidateNotNull()] - [string]$Source = 'Unknown', + [string]$Source = $([string]$parentFunctionName = [IO.Path]::GetFileNameWithoutExtension((Get-Variable -Name MyInvocation -Scope 1 -ErrorAction SilentlyContinue).Value.MyCommand.Name); If($parentFunctionName) {$parentFunctionName} Else {'Unknown'}), [Parameter(Mandatory=$false,Position=3)] [ValidateNotNullorEmpty()] [string]$ScriptSection = $script:installPhase, @@ -698,9 +768,10 @@ Function Write-Log { ## Logging Variables # Log file date/time - [string]$LogTime = (Get-Date -Format 'HH\:mm\:ss.fff').ToString() - [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString() - If (-not (Test-Path -LiteralPath 'variable:LogTimeZoneBias')) { [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes } + [datetime]$DateTimeNow = Get-Date + [string]$LogTime = $DateTimeNow.ToString('HH\:mm\:ss.fff') + [string]$LogDate = $DateTimeNow.ToString('MM-dd-yyyy') + If (-not (Test-Path -LiteralPath 'variable:LogTimeZoneBias')) { [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset($DateTimeNow).TotalMinutes } [string]$LogTimePlusBias = $LogTime + $script:LogTimeZoneBias # Initialize variables [boolean]$ExitLoggingFunction = $false @@ -881,6 +952,8 @@ Function Remove-InvalidFileNameChars { Remove invalid characters from the supplied string. .DESCRIPTION Remove invalid characters from the supplied string and returns a valid filename as a string. + .PARAMETER Name + Text to remove invalid filename characters from. .EXAMPLE Remove-InvalidFileNameChars -Name "Filename/\1" .NOTES @@ -1105,7 +1178,7 @@ Function Exit-Script { .PARAMETER ExitCode The exit code to be passed from the script to the parent process, e.g. SCCM .EXAMPLE - Exit-Script -ExitCode 0 + Exit-Script .EXAMPLE Exit-Script -ExitCode 1618 .NOTES @@ -1165,17 +1238,17 @@ Function Exit-Script { } Write-Log -Message "$installName $deploymentTypeName completed with exit code [$exitcode]." -Source ${CmdletName} - If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Info' -BalloonTipText $balloonText } + If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Info' -BalloonTipText $balloonText -NoWait } } ElseIf (-not $installSuccess) { Write-Log -Message "$installName $deploymentTypeName completed with exit code [$exitcode]." -Source ${CmdletName} If (($exitCode -eq $configInstallationUIExitCode) -or ($exitCode -eq $configInstallationDeferExitCode)) { [string]$balloonText = "$deploymentTypeName $configBalloonTextFastRetry" - If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Warning' -BalloonTipText $balloonText } + If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Warning' -BalloonTipText $balloonText -NoWait } } Else { [string]$balloonText = "$deploymentTypeName $configBalloonTextError" - If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Error' -BalloonTipText $balloonText } + If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Error' -BalloonTipText $balloonText -NoWait } } } @@ -1187,13 +1260,18 @@ Function Exit-Script { ## Disable logging to file so that we can archive the log files . $DisableScriptLogging - [string]$DestinationArchiveFileName = $installName + '_' + $deploymentType + '_' + ((Get-Date -Format 'yyyy-MM-dd-hh-mm-ss').ToString()) + '.zip' + [string]$DestinationArchiveFileName = $installName + '_' + $deploymentType + '_' + ((Get-Date -Format 'yyyy-MM-dd-HH-mm-ss').ToString()) + '.zip' New-ZipFile -DestinationArchiveDirectoryPath $configToolkitLogDir -DestinationArchiveFileName $DestinationArchiveFileName -SourceDirectory $logTempFolder -RemoveSourceAfterArchiving } If ($script:notifyIcon) { Try { $script:notifyIcon.Dispose() } Catch {} } ## Reset powershell window title to its previous title $Host.UI.RawUI.WindowTitle = $oldPSWindowTitle + ## Reset variables in case another toolkit is being run in the same session + $global:logName = $null + $global:installTitle = $null + $global:installName = $null + $global:appName = $null ## Exit the script, returning the exit code to SCCM If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $exitCode; Exit } Else { Exit $exitCode } } @@ -1273,7 +1351,7 @@ Function Resolve-Error { [string[]]$Property ) - [string[]]$ObjectProperty = $InputObject | Get-Member -MemberType '*Property' | Select-Object -ExpandProperty 'Name' + [string[]]$ObjectProperty = ($InputObject | Get-Member -MemberType '*Property').Name ForEach ($Prop in $Property) { If ($Prop -eq '*') { [string[]]$PropertySelection = $ObjectProperty @@ -1299,14 +1377,14 @@ Function Resolve-Error { ## Capture Error Record If ($GetErrorRecord) { [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord -Property $Property - $LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties + $LogErrorRecordMsg = $ErrRecord.$SelectedProperties } ## Error Invocation Information If ($GetErrorInvocation) { If ($ErrRecord.InvocationInfo) { [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property - $LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties + $LogErrorInvocationMsg = $ErrRecord.InvocationInfo.$SelectedProperties } } @@ -1314,7 +1392,7 @@ Function Resolve-Error { If ($GetErrorException) { If ($ErrRecord.Exception) { [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.Exception -Property $Property - $LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties + $LogErrorExceptionMsg = $ErrRecord.Exception.$SelectedProperties } } @@ -1352,7 +1430,7 @@ Function Resolve-Error { [string]$InnerExceptionSeperator = '~' * 40 [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrorInnerException -Property $Property - $LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String + $LogErrorInnerExceptionMsg = $ErrorInnerException.$SelectedProperties | Format-List | Out-String If ($Count -gt 0) { $LogInnerMessage += $InnerExceptionSeperator } $LogInnerMessage += $LogErrorInnerExceptionMsg @@ -1463,7 +1541,7 @@ Function Show-InstallationPrompt { Process { ## Bypass if in non-interactive mode If ($deployModeSilent) { - Write-Log -Message "Bypassing Installation Prompt [Mode: $deployMode]... $Message" -Source ${CmdletName} + Write-Log -Message "Bypassing Show-InstallationPrompt [Mode: $deployMode]. Message:$Message" -Source ${CmdletName} Return } @@ -1480,12 +1558,16 @@ Function Show-InstallationPrompt { [Windows.Forms.Application]::EnableVisualStyles() $formInstallationPrompt = New-Object -TypeName 'System.Windows.Forms.Form' $pictureBanner = New-Object -TypeName 'System.Windows.Forms.PictureBox' - $pictureIcon = New-Object -TypeName 'System.Windows.Forms.PictureBox' + If ($Icon -ne 'None') { + $pictureIcon = New-Object -TypeName 'System.Windows.Forms.PictureBox' + } $labelText = New-Object -TypeName 'System.Windows.Forms.Label' $buttonRight = New-Object -TypeName 'System.Windows.Forms.Button' $buttonMiddle = New-Object -TypeName 'System.Windows.Forms.Button' $buttonLeft = New-Object -TypeName 'System.Windows.Forms.Button' $buttonAbort = New-Object -TypeName 'System.Windows.Forms.Button' + $flowLayoutPanel = New-Object -TypeName 'System.Windows.Forms.FlowLayoutPanel' + $panelButtons = New-Object -TypeName 'System.Windows.Forms.Panel' $InitialFormInstallationPromptWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' [scriptblock]$Form_Cleanup_FormClosed = { @@ -1519,139 +1601,183 @@ Function Show-InstallationPrompt { } ## Form - $formInstallationPrompt.Controls.Add($pictureBanner) ##---------------------------------------------- ## Create padding object - $paddingNone = New-Object -TypeName 'System.Windows.Forms.Padding' - $paddingNone.Top = 0 - $paddingNone.Bottom = 0 - $paddingNone.Left = 0 - $paddingNone.Right = 0 + $paddingNone = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,0 + + ## Default control size + $DefaultControlSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,0 ## Generic Button properties - $buttonWidth = 110 - $buttonHeight = 23 - $buttonPadding = 50 - $buttonSize = New-Object -TypeName 'System.Drawing.Size' - $buttonSize.Width = $buttonWidth - $buttonSize.Height = $buttonHeight - $buttonPadding = New-Object -TypeName 'System.Windows.Forms.Padding' - $buttonPadding.Top = 0 - $buttonPadding.Bottom = 5 - $buttonPadding.Left = 50 - $buttonPadding.Right = 0 + $buttonSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 130,24 ## Picture Banner $pictureBanner.DataBindings.DefaultDataSourceUpdateMode = 0 $pictureBanner.ImageLocation = $appDeployLogoBanner - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 0 - $System_Drawing_Point.Y = 0 - $pictureBanner.Location = $System_Drawing_Point - $pictureBanner.Name = 'pictureBanner' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = $appDeployLogoBannerHeight - $System_Drawing_Size.Width = 450 - $pictureBanner.Size = $System_Drawing_Size + $pictureBanner.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,$appDeployLogoBannerHeight + $pictureBanner.MinimumSize = $DefaultControlSize $pictureBanner.SizeMode = 'CenterImage' $pictureBanner.Margin = $paddingNone - $pictureBanner.TabIndex = 0 $pictureBanner.TabStop = $false + $pictureBanner.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,0 ## Picture Icon - $pictureIcon.DataBindings.DefaultDataSourceUpdateMode = 0 - If ($icon -ne 'None') { $pictureIcon.Image = ([Drawing.SystemIcons]::$Icon).ToBitmap() } - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 15 - $System_Drawing_Point.Y = 105 + $appDeployLogoBannerHeightDifference - $pictureIcon.Location = $System_Drawing_Point - $pictureIcon.Name = 'pictureIcon' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 32 - $System_Drawing_Size.Width = 32 - $pictureIcon.Size = $System_Drawing_Size - $pictureIcon.AutoSize = $true - $pictureIcon.Margin = $paddingNone - $pictureIcon.TabIndex = 0 - $pictureIcon.TabStop = $false + If ($Icon -ne 'None') { + $pictureIcon.DataBindings.DefaultDataSourceUpdateMode = 0 + $pictureIcon.Image = ([Drawing.SystemIcons]::$Icon).ToBitmap() + $pictureIcon.Name = 'pictureIcon' + $pictureIcon.MinimumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 64,32 + $pictureIcon.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 64,32 + $pictureIcon.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 24,0,8,0 + $pictureIcon.SizeMode = "CenterImage" + $pictureIcon.TabStop = $false + $pictureIcon.Anchor = 'None' + $pictureIcon.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,10,0,5 + } ## Label Text $labelText.DataBindings.DefaultDataSourceUpdateMode = 0 $labelText.Name = 'labelText' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 148 - $System_Drawing_Size.Width = 385 + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' 386,0 $labelText.Size = $System_Drawing_Size - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 25 - $System_Drawing_Point.Y = $appDeployLogoBannerHeight - $labelText.Location = $System_Drawing_Point - $labelText.Margin = '0,0,0,0' - $labelText.Padding = '40,0,20,0' - $labelText.TabIndex = 1 + If ($Icon -ne 'None') { + $labelText.MinimumSize = New-Object -TypeName 'System.Drawing.Size' 386,$pictureIcon.Height + } else { + $labelText.MinimumSize = $System_Drawing_Size + } + $labelText.MaximumSize = $System_Drawing_Size + $labelText.AutoSize = $true + $labelText.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,10,0,5 + $labelText.TabStop = $false $labelText.Text = $message $labelText.TextAlign = "Middle$($MessageAlignment)" - $labelText.Anchor = 'Top' + $labelText.Anchor = 'None' $labelText.add_Click($handler_labelText_Click) - # Generic Y location for buttons - $buttonLocationY = 200 + $appDeployLogoBannerHeightDifference - + If ($Icon -ne 'None') { + # Add margin for the icon based on labelText Height so its centered + $pictureIcon.Height = $labelText.Height + } ## Button Left $buttonLeft.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonLeft.Location = "15,$buttonLocationY" $buttonLeft.Name = 'buttonLeft' $buttonLeft.Size = $buttonSize - $buttonLeft.TabIndex = 5 + $buttonLeft.MinimumSize = $buttonSize + $buttonLeft.MaximumSize = $buttonSize + $buttonLeft.TabIndex = 0 $buttonLeft.Text = $buttonLeftText $buttonLeft.DialogResult = 'No' $buttonLeft.AutoSize = $false + $buttonLeft.Margin = $paddingNone + $buttonLeft.Padding = $paddingNone $buttonLeft.UseVisualStyleBackColor = $true + $buttonLeft.Location = "14,4" $buttonLeft.add_Click($buttonLeft_OnClick) ## Button Middle $buttonMiddle.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonMiddle.Location = "170,$buttonLocationY" $buttonMiddle.Name = 'buttonMiddle' $buttonMiddle.Size = $buttonSize - $buttonMiddle.TabIndex = 6 + $buttonMiddle.MinimumSize = $buttonSize + $buttonMiddle.MaximumSize = $buttonSize + $buttonMiddle.TabIndex = 1 $buttonMiddle.Text = $buttonMiddleText $buttonMiddle.DialogResult = 'Ignore' $buttonMiddle.AutoSize = $true + $buttonMiddle.Margin = $paddingNone + $buttonMiddle.Padding = $paddingNone $buttonMiddle.UseVisualStyleBackColor = $true + $buttonMiddle.Location = "160,4" $buttonMiddle.add_Click($buttonMiddle_OnClick) ## Button Right $buttonRight.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonRight.Location = "325,$buttonLocationY" $buttonRight.Name = 'buttonRight' $buttonRight.Size = $buttonSize - $buttonRight.TabIndex = 7 + $buttonRight.MinimumSize = $buttonSize + $buttonRight.MaximumSize = $buttonSize + $buttonRight.TabIndex = 2 $buttonRight.Text = $ButtonRightText $buttonRight.DialogResult = 'Yes' $buttonRight.AutoSize = $true + $buttonRight.Margin = $paddingNone + $buttonRight.Padding = $paddingNone $buttonRight.UseVisualStyleBackColor = $true + $buttonRight.Location = "306,4" $buttonRight.add_Click($buttonRight_OnClick) ## Button Abort (Hidden) $buttonAbort.DataBindings.DefaultDataSourceUpdateMode = 0 $buttonAbort.Name = 'buttonAbort' - $buttonAbort.Size = '1,1' + $buttonAbort.Size = '0,0' + $buttonAbort.MinimumSize = '0,0' + $buttonAbort.MaximumSize = '0,0' + $buttonAbort.BackColor = [System.Drawing.Color]::Transparent + $buttonAbort.ForeColor = [System.Drawing.Color]::Transparent + $buttonAbort.FlatAppearance.BorderSize = 0; + $buttonAbort.FlatAppearance.MouseDownBackColor = [System.Drawing.Color]::Transparent + $buttonAbort.FlatAppearance.MouseOverBackColor = [System.Drawing.Color]::Transparent + $buttonAbort.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat $buttonAbort.DialogResult = 'Abort' $buttonAbort.TabStop = $false + $buttonAbort.Visible = $true # Has to be set visible so we can call Click on it + $buttonAbort.Margin = $paddingNone + $buttonAbort.Padding = $paddingNone $buttonAbort.UseVisualStyleBackColor = $true $buttonAbort.add_Click($buttonAbort_OnClick) + ## FlowLayoutPanel + $flowLayoutPanel.MinimumSize = $DefaultControlSize + $flowLayoutPanel.MaximumSize = $DefaultControlSize + $flowLayoutPanel.Size = $DefaultControlSize + $flowLayoutPanel.AutoSize = $true + $flowLayoutPanel.AutoSizeMode = "GrowAndShrink" + $flowLayoutPanel.Anchor = 'Top,Left' + $flowLayoutPanel.FlowDirection = 'LeftToRight' + $flowLayoutPanel.WrapContents = $true + $flowLayoutPanel.Margin = $paddingNone + $flowLayoutPanel.Padding = $paddingNone + ## Make sure label text is positioned correctly + If ($Icon -ne 'None') { + $labelText.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,10,0 + $pictureIcon.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,0 + $labelText.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 64,0 + } else { + $labelText.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelText.MinimumSize = $DefaultControlSize + $labelText.MaximumSize = $DefaultControlSize + $labelText.Size = $DefaultControlSize + $labelText.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,0 + } + If ($Icon -ne 'None') { + $flowLayoutPanel.Controls.Add($pictureIcon) + } + $flowLayoutPanel.Controls.Add($labelText) + $flowLayoutPanel.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,$appDeployLogoBannerHeight + + ## ButtonsPanel + $panelButtons.MinimumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,39 + $panelButtons.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,39 + $panelButtons.Padding = $paddingNone + $panelButtons.Margin = $paddingNone + $panelButtons.MaximumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,39 + $panelButtons.AutoSize = $true + If ($buttonLeftText) { $panelButtons.Controls.Add($buttonLeft) } + If ($buttonMiddleText) { $panelButtons.Controls.Add($buttonMiddle) } + If ($buttonRightText) { $panelButtons.Controls.Add($buttonRight) } + ## Add the ButtonsPanel to the flowLayoutPanel if any buttons are present + If ($buttonLeftText -or $buttonMiddleText -or $buttonRightText) { + $flowLayoutPanel.Controls.Add($panelButtons) + } + ## Form Installation Prompt - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 270 + $appDeployLogoBannerHeightDifference - $System_Drawing_Size.Width = 450 - $formInstallationPrompt.Size = $System_Drawing_Size - $formInstallationPrompt.Padding = '0,0,0,10' + $formInstallationPrompt.MinimumSize = $DefaultControlSize + $formInstallationPrompt.Size = $DefaultControlSize + $formInstallationPrompt.Padding = $paddingNone $formInstallationPrompt.Margin = $paddingNone $formInstallationPrompt.DataBindings.DefaultDataSourceUpdateMode = 0 - $formInstallationPrompt.Name = 'WelcomeForm' + $formInstallationPrompt.Name = 'InstallPromptForm' $formInstallationPrompt.Text = $title $formInstallationPrompt.StartPosition = 'CenterScreen' $formInstallationPrompt.FormBorderStyle = 'FixedDialog' @@ -1659,15 +1785,11 @@ Function Show-InstallationPrompt { $formInstallationPrompt.MinimizeBox = $false $formInstallationPrompt.TopMost = $true $formInstallationPrompt.TopLevel = $true + $formInstallationPrompt.AutoSize = $true $formInstallationPrompt.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon $formInstallationPrompt.Controls.Add($pictureBanner) - $formInstallationPrompt.Controls.Add($pictureIcon) - $formInstallationPrompt.Controls.Add($labelText) $formInstallationPrompt.Controls.Add($buttonAbort) - If ($buttonLeftText) { $formInstallationPrompt.Controls.Add($buttonLeft) } - If ($buttonMiddleText) { $formInstallationPrompt.Controls.Add($buttonMiddle) } - If ($buttonRightText) { $formInstallationPrompt.Controls.Add($buttonRight) } - + $formInstallationPrompt.Controls.Add($flowLayoutPanel) ## Timer $timer = New-Object -TypeName 'System.Windows.Forms.Timer' $timer.Interval = ($timeout * 1000) @@ -1700,8 +1822,10 @@ Function Show-InstallationPrompt { $timerPersist.Start() } - ## Close the Installation Progress Dialog if running - Close-InstallationProgress + if (-not $AsyncToolkitLaunch) { + ## Close the Installation Progress Dialog if running + Close-InstallationProgress + } [string]$installPromptLoggedParameters = ($installPromptParameters.GetEnumerator() | ForEach-Object { If ($_.Value.GetType().Name -eq 'SwitchParameter') { "-$($_.Key):`$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Boolean') { "-$($_.Key) `$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Int32') { "-$($_.Key) $($_.Value)" } Else { "-$($_.Key) `"$($_.Value)`"" } }) -join ' ' Write-Log -Message "Displaying custom installation prompt with the non-default parameters: [$installPromptLoggedParameters]." -Source ${CmdletName} @@ -1814,7 +1938,7 @@ Function Show-DialogBox { Process { # Bypass if in non-interactive mode If ($deployModeNonInteractive) { - Write-Log -Message "Bypassing Dialog Box [Mode: $deployMode]: $Text..." -Source ${CmdletName} + Write-Log -Message "Bypassing Show-DialogBox [Mode: $deployMode]. Text:$Text" -Source ${CmdletName} Return } @@ -2229,9 +2353,9 @@ Function Execute-MSI { .PARAMETER Path The path to the MSI/MSP file or the product code of the installed MSI. .PARAMETER Transform - The name of the transform file(s) to be applied to the MSI. The transform file is expected to be in the same directory as the MSI file. + The name of the transform file(s) to be applied to the MSI. The transform file is expected to be in the same directory as the MSI file. Multiple transforms have to be separated by a semi-colon. .PARAMETER Patch - The name of the patch (msp) file(s) to be applied to the MSI for use with the "Install" action. The patch file is expected to be in the same directory as the MSI file. + The name of the patch (msp) file(s) to be applied to the MSI for use with the "Install" action. The patch file is expected to be in the same directory as the MSI file. Multiple patches have to be separated by a semi-colon. .PARAMETER Parameters Overrides the default parameters specified in the XML configuration file. Install default is: "REBOOT=ReallySuppress /QB!". Uninstall default is: "REBOOT=ReallySuppress /QN". .PARAMETER AddParameters @@ -2449,13 +2573,12 @@ Function Execute-MSI { ## Enumerate all transforms specified, qualify the full path if possible and enclose in quotes If ($transform) { - [string[]]$transforms = $transform -split ',' - 0..($transforms.Length - 1) | ForEach-Object { - If (Test-Path -LiteralPath (Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $transforms[$_]) -PathType 'Leaf') { - $transforms[$_] = Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $transforms[$_].Replace('.\','') - } - Else { - $transforms[$_] = $transforms[$_] + [string[]]$transforms = $transform -replace "`"","" -split ';' + for ($i = 0; $i -lt $transforms.Length; $i++) { + [string]$FullPath = $null + [string]$FullPath = Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $transforms[$i].Replace('.\','') + If ($FullPath -and (Test-Path -LiteralPath $FullPath -PathType 'Leaf')) { + $transforms[$i] = $FullPath } } [string]$mstFile = "`"$($transforms -join ';')`"" @@ -2463,13 +2586,12 @@ Function Execute-MSI { ## Enumerate all patches specified, qualify the full path if possible and enclose in quotes If ($patch) { - [string[]]$patches = $patch -split ',' - 0..($patches.Length - 1) | ForEach-Object { - If (Test-Path -LiteralPath (Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $patches[$_]) -PathType 'Leaf') { - $patches[$_] = Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $patches[$_].Replace('.\','') - } - Else { - $patches[$_] = $patches[$_] + [string[]]$patches = $patch -replace "`"","" -split ';' + for ($i = 0; $i -lt $patches.Length; $i++) { + [string]$FullPath = $null + [string]$FullPath = Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $patches[$i].Replace('.\','') + If ($FullPath -and (Test-Path -LiteralPath $FullPath -PathType 'Leaf')) { + $patches[$i] = $FullPath } } [string]$mspFile = "`"$($patches -join ';')`"" @@ -2501,6 +2623,8 @@ Function Execute-MSI { If ($patch) { $argsMSI = "$argsMSI PATCH=$mspFile" } # Replace default parameters if specified. If ($Parameters) { $argsMSI = "$argsMSI $Parameters" } Else { $argsMSI = "$argsMSI $msiDefaultParams" } + # Add reinstallmode and reinstall variable for Patch + If ($action -eq 'Patch') {$argsMSI += " REINSTALLMODE=ecmus REINSTALL=ALL"} # Append parameters to default parameters if specified. If ($AddParameters) { $argsMSI = "$argsMSI $AddParameters" } # Add custom Logging Options if specified, otherwise, add default Logging Options from Config file @@ -2531,17 +2655,20 @@ Function Execute-MSI { ElseIf (((-not $IsMsiInstalled) -and ($Action -eq 'Install')) -or ($IsMsiInstalled)) { Write-Log -Message "Executing MSI action [$Action]..." -Source ${CmdletName} # Build the hashtable with the options that will be passed to Execute-Process using splatting - [hashtable]$ExecuteProcessSplat = @{ Path = $exeMsiexec - Parameters = $argsMSI - WindowStyle = 'Normal' } + [hashtable]$ExecuteProcessSplat = @{ + Path = $exeMsiexec + Parameters = $argsMSI + WindowStyle = 'Normal' + ExitOnProcessFailure = $ExitOnProcessFailure + ContinueOnError = $ContinueOnError + } If ($WorkingDirectory) { $ExecuteProcessSplat.Add( 'WorkingDirectory', $WorkingDirectory) } - If ($ContinueOnError) { $ExecuteProcessSplat.Add( 'ContinueOnError', $ContinueOnError) } If ($SecureParameters) { $ExecuteProcessSplat.Add( 'SecureParameters', $SecureParameters) } If ($PassThru) { $ExecuteProcessSplat.Add( 'PassThru', $PassThru) } If ($IgnoreExitCodes) { $ExecuteProcessSplat.Add( 'IgnoreExitCodes', $IgnoreExitCodes) } If ($PriorityClass) { $ExecuteProcessSplat.Add( 'PriorityClass', $PriorityClass) } - If ($ExitOnProcessFailure) { $ExecuteProcessSplat.Add( 'ExitOnProcessFailure', $ExitOnProcessFailure) } If ($NoWait) { $ExecuteProcessSplat.Add( 'NoWait', $NoWait) } + # Call the Execute-Process function If ($PassThru) { [psobject]$ExecuteResults = Execute-Process @ExecuteProcessSplat @@ -2765,8 +2892,11 @@ Function Remove-MSIApplications { } ## Build the hashtable with the options that will be passed to Execute-MSI using splatting - [hashtable]$ExecuteMSISplat = @{ Action = 'Uninstall'; Path = '' } - If ($ContinueOnError) { $ExecuteMSISplat.Add( 'ContinueOnError', $ContinueOnError) } + [hashtable]$ExecuteMSISplat = @{ + Action = 'Uninstall' + Path = '' + ContinueOnError = $ContinueOnError + } If ($Parameters) { $ExecuteMSISplat.Add( 'Parameters', $Parameters) } ElseIf ($AddParameters) { $ExecuteMSISplat.Add( 'AddParameters', $AddParameters) } If ($LoggingOptions) { $ExecuteMSISplat.Add( 'LoggingOptions', $LoggingOptions) } @@ -2815,15 +2945,18 @@ Function Execute-Process { Hides all parameters passed to the executable from the Toolkit log file .PARAMETER WindowStyle Style of the window of the process executed. Options: Normal, Hidden, Maximized, Minimized. Default: Normal. - Note: Not all processes honor the "Hidden" flag. If it it not working, then check the command line options for the process being executed to see it has a silent option. + Note: Not all processes honor WindowStyle. WindowStyle is a recommendation passed to the process. They can choose to ignore it. + Only works for native Windows GUI applications. If the WindowStyle is set to Hidden, UseShellExecute should be set to $true. .PARAMETER CreateNoWindow - Specifies whether the process should be started with a new window to contain it. Default is false. + Specifies whether the process should be started with a new window to contain it. Only works for Console mode applications. UseShellExecute should be set to $false. + Default is false. .PARAMETER WorkingDirectory The working directory used for executing the process. Defaults to the directory of the file being executed. + Parameter UseShellExecute affects this parameter. .PARAMETER NoWait Immediately continue after executing the process. .PARAMETER PassThru - Returns ExitCode, STDOut, and STDErr output from the process. + If NoWait is not specified, returns an object with ExitCode, STDOut and STDErr output from the process. If NoWait is specified, returns an object with Id, Handle and ProcessName. .PARAMETER WaitForMsiExec Sometimes an EXE bootstrapper will launch an MSI install. In such cases, this variable will ensure that this function waits for the msiexec engine to become available before starting the install. @@ -2835,6 +2968,14 @@ Function Execute-Process { Specifies priority class for the process. Options: Idle, Normal, High, AboveNormal, BelowNormal, RealTime. Default: Normal .PARAMETER ExitOnProcessFailure Specifies whether the function should call Exit-Script when the process returns an exit code that is considered an error/failure. Default: $true +.PARAMETER UseShellExecute + Specifies whether to use the operating system shell to start the process. $true if the shell should be used when starting the process; $false if the process should be created directly from the executable file. + The word "Shell" in this context refers to a graphical shell (similar to the Windows shell) rather than command shells (for example, bash or sh) and lets users launch graphical applications or open documents. + It lets you open a file or a url and the Shell will figure out the program to open it with. + The WorkingDirectory property behaves differently depending on the value of the UseShellExecute property. When UseShellExecute is true, the WorkingDirectory property specifies the location of the executable. + When UseShellExecute is false, the WorkingDirectory property is not used to find the executable. Instead, it is used only by the process that is started and has meaning only within the context of the new process. + If you set UseShellExecute to $true, there will be no available output from the process. + Default: $false .PARAMETER ContinueOnError Continue if an error occured while trying to start the process. Default: $false. .EXAMPLE @@ -2892,7 +3033,10 @@ Function Execute-Process { [Diagnostics.ProcessPriorityClass]$PriorityClass = 'Normal', [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] - [boolean]$ExitOnProcessFailure = $true, + [boolean]$ExitOnProcessFailure = $true, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$UseShellExecute = $false, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [boolean]$ContinueOnError = $false @@ -2978,22 +3122,29 @@ Function Execute-Process { $processStartInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' -ErrorAction 'Stop' $processStartInfo.FileName = $Path $processStartInfo.WorkingDirectory = $WorkingDirectory - $processStartInfo.UseShellExecute = $false + $processStartInfo.UseShellExecute = $UseShellExecute $processStartInfo.ErrorDialog = $false $processStartInfo.RedirectStandardOutput = $true $processStartInfo.RedirectStandardError = $true $processStartInfo.CreateNoWindow = $CreateNoWindow If ($Parameters) { $processStartInfo.Arguments = $Parameters } - If ($windowStyle) { $processStartInfo.WindowStyle = $WindowStyle } + $processStartInfo.WindowStyle = $WindowStyle + If ($processStartInfo.UseShellExecute -eq $true) { + Write-Log -Message "UseShellExecute is set to true, standard output and error will not be available." -Source ${CmdletName} + $processStartInfo.RedirectStandardOutput = $false + $processStartInfo.RedirectStandardError = $false + } $process = New-Object -TypeName 'System.Diagnostics.Process' -ErrorAction 'Stop' $process.StartInfo = $processStartInfo - ## Add event handler to capture process's standard output redirection - [scriptblock]$processEventHandler = { If (-not [string]::IsNullOrEmpty($EventArgs.Data)) { $Event.MessageData.AppendLine($EventArgs.Data) } } - $stdOutBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList '' - $stdOutEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'OutputDataReceived' -MessageData $stdOutBuilder -ErrorAction 'Stop' - $stdErrBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList '' - $stdErrEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'ErrorDataReceived' -MessageData $stdErrBuilder -ErrorAction 'Stop' + If ($processStartInfo.UseShellExecute -eq $false) { + ## Add event handler to capture process's standard output redirection + [scriptblock]$processEventHandler = { If (-not [string]::IsNullOrEmpty($EventArgs.Data)) { $Event.MessageData.AppendLine($EventArgs.Data) } } + $stdOutBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList '' + $stdOutEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'OutputDataReceived' -MessageData $stdOutBuilder -ErrorAction 'Stop' + $stdErrBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList '' + $stdErrEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'ErrorDataReceived' -MessageData $stdErrBuilder -ErrorAction 'Stop' + } ## Start Process Write-Log -Message "Working Directory is [$WorkingDirectory]." -Source ${CmdletName} @@ -3015,7 +3166,7 @@ Function Execute-Process { } $null = $process.Start() - + ## Set priority If ($PriorityClass -ne "Normal") { try { If ($process.HasExited -eq $False) { @@ -3031,14 +3182,26 @@ Function Execute-Process { Write-Log -Message "Failed to change the priority class for the process." -Severity 2 -Source ${CmdletName} } } - + ## NoWait specified, return process details. If it isnt specified, start reading standard Output and Error streams If ($NoWait) { Write-Log -Message 'NoWait parameter specified. Continuing without waiting for exit code...' -Source ${CmdletName} + + If ($PassThru) { + If ($process.HasExited -eq $false) { + Write-Log -Message "PassThru parameter specified, returning process details object." -Source ${CmdletName} + [psobject]$ProcessDetails = New-Object -TypeName 'PSObject' -Property @{ Id = If ($process.Id) {$process.Id} Else { $null } ; Handle = If ($process.Handle) { $process.Handle } Else { [IntPtr]::Zero }; ProcessName = If ($process.ProcessName) { $process.ProcessName } Else { '' } } + Write-Output -InputObject $ProcessDetails + } + Else { + Write-Log -Message "PassThru parameter specified, however the process has already exited." -Source ${CmdletName} + } + } } Else { - $process.BeginOutputReadLine() - $process.BeginErrorReadLine() - + If ($processStartInfo.UseShellExecute -eq $false) { + $process.BeginOutputReadLine() + $process.BeginErrorReadLine() + } ## Instructs the Process component to wait indefinitely for the associated process to exit. $process.WaitForExit() @@ -3054,21 +3217,25 @@ Function Execute-Process { [int32]$returnCode = 60013 } - ## Unregister standard output and error event to retrieve process output - If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null } - If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null } - $stdOut = $stdOutBuilder.ToString() -replace $null,'' - $stdErr = $stdErrBuilder.ToString() -replace $null,'' - - If ($stdErr.Length -gt 0) { - Write-Log -Message "Standard error output from the process: $stdErr" -Severity 3 -Source ${CmdletName} + If ($processStartInfo.UseShellExecute -eq $false) { + ## Unregister standard output and error event to retrieve process output + If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null } + If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null } + $stdOut = $stdOutBuilder.ToString() -replace $null,'' + $stdErr = $stdErrBuilder.ToString() -replace $null,'' + + If ($stdErr.Length -gt 0) { + Write-Log -Message "Standard error output from the process: $stdErr" -Severity 3 -Source ${CmdletName} + } } } } Finally { - ## Make sure the standard output and error event is unregistered - If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null } - If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null } + If ($processStartInfo.UseShellExecute -eq $false) { + ## Make sure the standard output and error event is unregistered + If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null } + If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null } + } ## Free resources associated with the process, this does not cause process to exit If ($process) { $process.Dispose() } @@ -3097,8 +3264,8 @@ Function Execute-Process { ## If the passthru switch is specified, return the exit code and any output from process If ($PassThru) { - Write-Log -Message "-PassThru parameter specified, returning execution results object." -Source ${CmdletName} - [psobject]$ExecutionResults = New-Object -TypeName 'PSObject' -Property @{ ExitCode = $returnCode; StdOut = $stdOut; StdErr = $stdErr } + Write-Log -Message "PassThru parameter specified, returning execution results object." -Source ${CmdletName} + [psobject]$ExecutionResults = New-Object -TypeName 'PSObject' -Property @{ ExitCode = $returnCode; StdOut = If ($stdOut) { $stdOut } Else { '' }; StdErr = If ($stdErr) { $stdErr } Else { '' } } Write-Output -InputObject $ExecutionResults } @@ -3314,7 +3481,7 @@ Function Test-IsMutexAvailable { If ($MutexName -eq 'Global\_MSIExecute') { ## Get the command line for the MSI installation in progress Try { - [string]$msiInProgressCmdLine = Get-WmiObject -Class 'Win32_Process' -Filter "name = 'msiexec.exe'" -ErrorAction 'Stop' | Where-Object { $_.CommandLine } | Select-Object -ExpandProperty 'CommandLine' | Where-Object { $_ -match '\.msi' } | ForEach-Object { $_.Trim() } + [string]$msiInProgressCmdLine = Get-WmiObject -Class 'Win32_Process' -Filter "name = 'msiexec.exe'" -ErrorAction 'Stop' | ForEach-Object { if ($_.CommandLine -match '\.msi') {$_.CommandLine.Trim()} } } Catch { } Write-Log -Message "Mutex [$MutexName] is not available for an exclusive lock because the following MSI installation is in progress [$msiInProgressCmdLine]." -Severity 2 -Source ${CmdletName} @@ -3486,7 +3653,7 @@ Function Copy-File { .PARAMETER ContinueFileCopyOnError Continue copying files if an error is encountered. This will continue the deployment script and will warn about files that failed to be copied. Default is: $false. .EXAMPLE - Copy-File -Path "$dirSupportFiles\MyApp.ini" -Destination "$envWindir\MyApp.ini" + Copy-File -Path "$dirSupportFiles\MyApp.ini" -Destination "$envWinDir\MyApp.ini" .EXAMPLE Copy-File -Path "$dirSupportFiles\*.*" -Destination "$envTemp\tempfiles" Copy all of the files in a folder to a destination folder. @@ -3520,7 +3687,6 @@ Function Copy-File { } Process { Try { - $null = $fileCopyError If ((-not ([IO.Path]::HasExtension($Destination))) -and (-not (Test-Path -LiteralPath $Destination -PathType 'Container'))) { Write-Log -Message "Destination folder does not exist, creating destination folder [$destination]." -Source ${CmdletName} $null = New-Item -Path $Destination -Type 'Directory' -Force -ErrorAction 'Stop' @@ -3530,13 +3696,17 @@ Function Copy-File { If ($Recurse) { Write-Log -Message "Copy file(s) recursively in path [$path] to destination [$destination] root folder, flattened." -Source ${CmdletName} If (-not $ContinueFileCopyOnError) { - $null = Get-ChildItem -Path $path -Recurse | Where-Object {!($_.PSIsContainer)} | ForEach-Object { - Copy-Item -Path ($_.FullName) -Destination $destination -Force -ErrorAction 'Stop' + $null = Get-ChildItem -Path $path -Recurse | ForEach-Object { + if(-not($_.PSIsContainer)) { + Copy-Item -Path ($_.FullName) -Destination $destination -Force -ErrorAction 'Stop' + } } } Else { - $null = Get-ChildItem -Path $path -Recurse | Where-Object {!($_.PSIsContainer)} | ForEach-Object { - Copy-Item -Path ($_.FullName) -Destination $destination -Force -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError + $null = Get-ChildItem -Path $path -Recurse | ForEach-Object { + if(-not($_.PSIsContainer)) { + Copy-Item -Path ($_.FullName) -Destination $destination -Force -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError + } } } } @@ -3654,10 +3824,10 @@ Function Remove-File { ForEach ($Item in $SpecifiedPath) { Try { If ($PSCmdlet.ParameterSetName -eq 'Path') { - [string[]]$ResolvedPath += Resolve-Path -Path $Item -ErrorAction 'Stop' | Where-Object { $_.Path } | Select-Object -ExpandProperty 'Path' -ErrorAction 'Stop' + [string[]]$ResolvedPath += Resolve-Path -Path $Item -ErrorAction 'Stop' | ForEach-Object { if($_.Path) {$_.Path} } } Else { - [string[]]$ResolvedPath += Resolve-Path -LiteralPath $Item -ErrorAction 'Stop' | Where-Object { $_.Path } | Select-Object -ExpandProperty 'Path' -ErrorAction 'Stop' + [string[]]$ResolvedPath += Resolve-Path -LiteralPath $Item -ErrorAction 'Stop' | ForEach-Object { if($_.Path) {$_.Path} } } } Catch [System.Management.Automation.ItemNotFoundException] { @@ -3679,8 +3849,8 @@ Function Remove-File { Write-Log -Message "Delete file(s) recursively in path [$Item]..." -Source ${CmdletName} } ElseIf ((-not $Recurse) -and (Test-Path -LiteralPath $Item -PathType 'Container')) { - Write-Log -Message "Skipping folder [$Item] because the Recurse switch was not specified" - Continue + Write-Log -Message "Skipping folder [$Item] because the Recurse switch was not specified" -Source ${CmdletName} + Continue } Else { Write-Log -Message "Delete file in path [$Item]..." -Source ${CmdletName} @@ -3720,6 +3890,8 @@ Function Convert-RegistryPath { .PARAMETER SID The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. +.PARAMETER DisableFunctionLogging + Disables logging of this function. Default: $true .EXAMPLE Convert-RegistryPath -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' .EXAMPLE @@ -3735,7 +3907,10 @@ Function Convert-RegistryPath { [string]$Key, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] - [string]$SID + [string]$SID, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [bool]$DisableFunctionLogging = $true ) Begin { @@ -3745,45 +3920,41 @@ Function Convert-RegistryPath { } Process { ## Convert the registry key hive to the full path, only match if at the beginning of the line - If ($Key -match '^HKLM:\\|^HKCU:\\|^HKCR:\\|^HKU:\\|^HKCC:\\|^HKPD:\\') { - # Converts registry paths that start with, e.g.: HKLM:\ - $key = $key -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' - $key = $key -replace '^HKCR:\\', 'HKEY_CLASSES_ROOT\' - $key = $key -replace '^HKCU:\\', 'HKEY_CURRENT_USER\' - $key = $key -replace '^HKU:\\', 'HKEY_USERS\' - $key = $key -replace '^HKCC:\\', 'HKEY_CURRENT_CONFIG\' - $key = $key -replace '^HKPD:\\', 'HKEY_PERFORMANCE_DATA\' - } - ElseIf ($Key -match '^HKLM:|^HKCU:|^HKCR:|^HKU:|^HKCC:|^HKPD:') { - # Converts registry paths that start with, e.g.: HKLM: - $key = $key -replace '^HKLM:', 'HKEY_LOCAL_MACHINE\' - $key = $key -replace '^HKCR:', 'HKEY_CLASSES_ROOT\' - $key = $key -replace '^HKCU:', 'HKEY_CURRENT_USER\' - $key = $key -replace '^HKU:', 'HKEY_USERS\' - $key = $key -replace '^HKCC:', 'HKEY_CURRENT_CONFIG\' - $key = $key -replace '^HKPD:', 'HKEY_PERFORMANCE_DATA\' - } - ElseIf ($Key -match '^HKLM\\|^HKCU\\|^HKCR\\|^HKU\\|^HKCC\\|^HKPD\\') { - # Converts registry paths that start with, e.g.: HKLM\ - $key = $key -replace '^HKLM\\', 'HKEY_LOCAL_MACHINE\' - $key = $key -replace '^HKCR\\', 'HKEY_CLASSES_ROOT\' - $key = $key -replace '^HKCU\\', 'HKEY_CURRENT_USER\' - $key = $key -replace '^HKU\\', 'HKEY_USERS\' - $key = $key -replace '^HKCC\\', 'HKEY_CURRENT_CONFIG\' - $key = $key -replace '^HKPD\\', 'HKEY_PERFORMANCE_DATA\' + If ($Key -match '^HKLM') { + $Key = $Key -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' -replace '^HKLM:', 'HKEY_LOCAL_MACHINE\' -replace '^HKLM\\', 'HKEY_LOCAL_MACHINE\' + } + elseif ($Key -match '^HKCR') { + $Key = $Key -replace '^HKCR:\\', 'HKEY_CLASSES_ROOT\' -replace '^HKCR:', 'HKEY_CLASSES_ROOT\' -replace '^HKCR\\', 'HKEY_CLASSES_ROOT\' + } + elseif ($Key -match '^HKCU') { + $Key = $Key -replace '^HKCU:\\', 'HKEY_CURRENT_USER\' -replace '^HKCU:', 'HKEY_CURRENT_USER\' -replace '^HKCU\\', 'HKEY_CURRENT_USER\' + } + elseif ($Key -match '^HKU') { + $Key = $Key -replace '^HKU:\\', 'HKEY_USERS\' -replace '^HKU:', 'HKEY_USERS\' -replace '^HKU\\', 'HKEY_USERS\' } + elseif ($Key -match '^HKCC') { + $Key = $Key -replace '^HKCC:\\', 'HKEY_CURRENT_CONFIG\' -replace '^HKCC:', 'HKEY_CURRENT_CONFIG\' -replace '^HKCC\\', 'HKEY_CURRENT_CONFIG\' + } + elseif ($Key -match '^HKPD') { + $Key = $Key -replace '^HKPD:\\', 'HKEY_PERFORMANCE_DATA\' -replace '^HKPD:', 'HKEY_PERFORMANCE_DATA\' -replace '^HKPD\\', 'HKEY_PERFORMANCE_DATA\' + } + + ## Append the PowerShell provider to the registry key path + If ($key -notmatch '^Registry::') {[string]$key = "Registry::$key" } If ($PSBoundParameters.ContainsKey('SID')) { ## If the SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID - If ($key -match '^HKEY_CURRENT_USER\\') { $key = $key -replace '^HKEY_CURRENT_USER\\', "HKEY_USERS\$SID\" } + If ($key -match '^Registry::HKEY_CURRENT_USER\\') { $key = $key -replace '^Registry::HKEY_CURRENT_USER\\', "Registry::HKEY_USERS\$SID\" } + Elseif (-not ($DisableFunctionLogging)) { + Write-Log -Message "SID parameter specified but the registry hive of the key is not HKEY_CURRENT_USER." -Source ${CmdletName} -Severity 2 + } } - ## Append the PowerShell drive to the registry key path - If ($key -notmatch '^Registry::') {[string]$key = "Registry::$key" } - If($Key -match '^Registry::HKEY_LOCAL_MACHINE|^Registry::HKEY_CLASSES_ROOT|^Registry::HKEY_CURRENT_USER|^Registry::HKEY_USERS|^Registry::HKEY_CURRENT_CONFIG|^Registry::HKEY_PERFORMANCE_DATA') { ## Check for expected key string format - Write-Log -Message "Return fully qualified registry key path [$key]." -Source ${CmdletName} + If (-not ($DisableFunctionLogging)) { + Write-Log -Message "Return fully qualified registry key path [$key]." -Source ${CmdletName} + } Write-Output -InputObject $key } Else{ @@ -3960,7 +4131,7 @@ Function Get-RegistryKey { ## Get all property values for registry key $regKeyValue = Get-ItemProperty -LiteralPath $key -ErrorAction 'Stop' - [int32]$regKeyValuePropertyCount = $regKeyValue | Measure-Object | Select-Object -ExpandProperty 'Count' + [int32]$regKeyValuePropertyCount = ($regKeyValue | Measure-Object).Count ## Select requested property If ($PSBoundParameters.ContainsKey('Value')) { @@ -3968,7 +4139,7 @@ Function Get-RegistryKey { [boolean]$IsRegistryValueExists = $false If ($regKeyValuePropertyCount -gt 0) { Try { - [string[]]$PathProperties = Get-Item -LiteralPath $Key -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Property' -ErrorAction 'Stop' + [string[]]$PathProperties = (Get-Item -LiteralPath $Key -ErrorAction 'Stop').Property If ($PathProperties -contains $Value) { $IsRegistryValueExists = $true } } Catch { } @@ -3988,7 +4159,7 @@ Function Get-RegistryKey { $regKeyValue = $(Get-Item -LiteralPath $key -ErrorAction 'Stop').GetValue($null) } Else { - $regKeyValue = $regKeyValue | Select-Object -ExpandProperty $Value -ErrorAction 'SilentlyContinue' + $regKeyValue = $regKeyValue.$Value } } Else { @@ -4064,7 +4235,7 @@ Function Set-RegistryKey { .EXAMPLE Set-RegistryKey -Key 'HKCU\Software\Microsoft\Example' -Name 'Data' -Value (0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x02,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x02,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x02,0x02,0x02) -Type 'Binary' .EXAMPLE - Set-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Example' -Value '(Default)' + Set-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Example' -Name '(Default)' -Value "Text" .NOTES .LINK http://psappdeploytoolkit.com @@ -4119,7 +4290,7 @@ Function Set-RegistryKey { # Forward slash was found in Key. Use REG.exe ADD to create registry key Else { - [string]$CreateRegkeyResult = & reg.exe Add "$($Key.Substring($Key.IndexOf('::') + 2))" + [string]$CreateRegkeyResult = & "$envWinDir\System32\reg.exe" Add "$($Key.Substring($Key.IndexOf('::') + 2))" If ($global:LastExitCode -ne 0) { Throw "Failed to create registry key [$Key]" @@ -4195,6 +4366,8 @@ Function Remove-RegistryKey { Remove-RegistryKey -Key 'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce' .EXAMPLE Remove-RegistryKey -Key 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -Name 'RunAppInstall' +.EXAMPLE + Remove-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Example' -Name '(Default)' .NOTES .LINK http://psappdeploytoolkit.com @@ -4350,7 +4523,7 @@ Function Invoke-HKCURegistrySettingsForAllUsers { # Load the User registry hive if the registry hive file exists If (Test-Path -LiteralPath $UserRegistryHiveFile -PathType 'Leaf') { Write-Log -Message "Load the User [$($UserProfile.NTAccount)] registry hive in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} - [string]$HiveLoadResult = & reg.exe load "`"HKEY_USERS\$($UserProfile.SID)`"" "`"$UserRegistryHiveFile`"" + [string]$HiveLoadResult = & "$envWinDir\System32\reg.exe" load "`"HKEY_USERS\$($UserProfile.SID)`"" "`"$UserRegistryHiveFile`"" If ($global:LastExitCode -ne 0) { Throw "Failed to load the registry hive for User [$($UserProfile.NTAccount)] with SID [$($UserProfile.SID)]. Failure message [$HiveLoadResult]. Continue..." @@ -4379,7 +4552,7 @@ Function Invoke-HKCURegistrySettingsForAllUsers { If ($ManuallyLoadedRegHive) { Try { Write-Log -Message "Unload the User [$($UserProfile.NTAccount)] registry hive in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} - [string]$HiveLoadResult = & reg.exe unload "`"HKEY_USERS\$($UserProfile.SID)`"" + [string]$HiveLoadResult = & "$envWinDir\System32\reg.exe" unload "`"HKEY_USERS\$($UserProfile.SID)`"" If ($global:LastExitCode -ne 0) { Write-Log -Message "REG.exe failed to unload the registry hive and exited with exit code [$($global:LastExitCode)]. Performing manual garbage collection to ensure successful unloading of registry hive." -Severity 2 -Source ${CmdletName} @@ -4388,7 +4561,7 @@ Function Invoke-HKCURegistrySettingsForAllUsers { Start-Sleep -Seconds 5 Write-Log -Message "Unload the User [$($UserProfile.NTAccount)] registry hive in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} - [string]$HiveLoadResult = & reg.exe unload "`"HKEY_USERS\$($UserProfile.SID)`"" + [string]$HiveLoadResult = & "$envWinDir\System32\reg.exe" unload "`"HKEY_USERS\$($UserProfile.SID)`"" If ($global:LastExitCode -ne 0) { Throw "REG.exe failed with exit code [$($global:LastExitCode)] and result [$HiveLoadResult]." } } } @@ -4579,8 +4752,10 @@ Function Get-UserProfiles { [string]$UserProfileListRegKey = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' [psobject[]]$UserProfiles = Get-ChildItem -LiteralPath $UserProfileListRegKey -ErrorAction 'Stop' | ForEach-Object { - Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction 'Stop' | Where-Object { ($_.ProfileImagePath) } | + $ProfileProperties = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction 'Stop' | Where-Object { ($_.ProfileImagePath) } | Select-Object @{ Label = 'NTAccount'; Expression = { $(ConvertTo-NTAccountOrSID -SID $_.PSChildName).Value } }, @{ Label = 'SID'; Expression = { $_.PSChildName } }, @{ Label = 'ProfilePath'; Expression = { $_.ProfileImagePath } } + ## This removes "defaultuser0" account, which is Windows's 10 bug + if ($ProfileProperties.NTAccount) {$ProfileProperties} } If ($ExcludeSystemProfiles) { [string[]]$SystemProfiles = 'S-1-5-18', 'S-1-5-19', 'S-1-5-20' @@ -4592,17 +4767,17 @@ Function Get-UserProfiles { ## Find the path to the Default User profile If (-not $ExcludeDefaultUser) { - [string]$UserProfilesDirectory = Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'ProfilesDirectory' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'ProfilesDirectory' + [string]$UserProfilesDirectory = (Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'ProfilesDirectory' -ErrorAction 'Stop').ProfilesDirectory # On Windows Vista or higher If (([version]$envOSVersion).Major -gt 5) { # Path to Default User Profile directory on Windows Vista or higher: By default, C:\Users\Default - [string]$DefaultUserProfileDirectory = Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'Default' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Default' + [string]$DefaultUserProfileDirectory = (Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'Default' -ErrorAction 'Stop').Default } # On Windows XP or lower Else { # Default User Profile Name: By default, 'Default User' - [string]$DefaultUserProfileName = Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'DefaultUserProfile' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'DefaultUserProfile' + [string]$DefaultUserProfileName = (Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'DefaultUserProfile' -ErrorAction 'Stop').DefaultUserProfile # Path to Default User Profile directory: By default, C:\Documents and Settings\Default User [string]$DefaultUserProfileDirectory = Join-Path -Path $UserProfilesDirectory -ChildPath $DefaultUserProfileName @@ -4731,7 +4906,7 @@ Function New-Shortcut { .PARAMETER IconLocation Location of the icon used for the shortcut .PARAMETER IconIndex - Executables, DLLs, ICO files with multiple icons need the icon index to be specified + The index of the icon. Executables, DLLs, ICO files with multiple icons need the icon index to be specified. This parameter is an Integer. The first index is 0. .PARAMETER Description Description of the shortcut .PARAMETER WorkingDirectory @@ -4741,18 +4916,19 @@ Function New-Shortcut { .PARAMETER RunAsAdmin Set shortcut to run program as administrator. This option will prompt user to elevate when executing shortcut. .PARAMETER Hotkey - Create a Hotkey to launch the shortcut, e.g. "CTRL+SHIFT+F" + Create a Hotkey to launch the shortcut, e.g. "CTRL+SHIFT+F" .PARAMETER ContinueOnError Continue if an error is encountered. Default is: $true. .EXAMPLE New-Shortcut -Path "$envProgramData\Microsoft\Windows\Start Menu\My Shortcut.lnk" -TargetPath "$envWinDir\system32\notepad.exe" -IconLocation "$envWinDir\system32\notepad.exe" -Description 'Notepad' -WorkingDirectory "$envHomeDrive\$envHomePath" .NOTES + Url shortcuts only support TargetPath, IconLocation and IconIndex. Other parameters are ignored. .LINK http://psappdeploytoolkit.com #> [CmdletBinding()] Param ( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory=$true, Position=0)] [ValidateNotNullorEmpty()] [string]$Path, [Parameter(Mandatory=$true)] @@ -4766,7 +4942,7 @@ Function New-Shortcut { [string]$IconLocation, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] - [string]$IconIndex, + [int]$IconIndex, [Parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [string]$Description, @@ -4795,12 +4971,42 @@ Function New-Shortcut { } Process { Try { + $extension = [IO.Path]::GetExtension($Path).ToLower() + If ((-not $extension) -or (($extension -ne '.lnk') -and ($extension -ne '.url'))) { + Write-Log -Message "Specified file [$Path] does not have a valid shortcut extension: .url .lnk" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw + } + return + } Try { - [IO.FileInfo]$Path = [IO.FileInfo]$Path - [string]$PathDirectory = $Path.DirectoryName + # Make sure Net framework current dir is synced with powershell cwd + [IO.Directory]::SetCurrentDirectory((Get-Location)) + # Get full path + [string]$FullPath = [IO.Path]::GetFullPath($Path) + } + Catch { + Write-Log -Message "Specified path [$Path] is not valid." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw + } + return + } - If (-not (Test-Path -LiteralPath $PathDirectory -PathType 'Container' -ErrorAction 'Stop')) { - Write-Log -Message "Create shortcut directory [$PathDirectory]." -Source ${CmdletName} + Try { + [string]$PathDirectory = [IO.Path]::GetDirectoryName($FullPath) + If (-not $PathDirectory) { + # The path is root or no filename supplied + If (-not [IO.Path]::GetFileNameWithoutExtension($FullPath)) { + # No filename supplied + If (-not $ContinueOnError) { + Throw + } + return + } + # Continue without creating a folder because the path is root + } ElseIf (-not (Test-Path -LiteralPath $PathDirectory -PathType 'Container' -ErrorAction 'Stop')) { + Write-Log -Message "Creating shortcut directory [$PathDirectory]." -Source ${CmdletName} $null = New-Item -Path $PathDirectory -ItemType 'Directory' -Force -ErrorAction 'Stop' } } @@ -4809,57 +5015,59 @@ Function New-Shortcut { Throw } - Write-Log -Message "Create shortcut [$($path.FullName)]." -Source ${CmdletName} - If (($path.FullName).ToLower().EndsWith('.url')) { + If (Test-Path -Path $FullPath -PathType Leaf) { + Write-Log -Message "The shortcut [$FullPath] already exists. Deleting the file..." -Source ${CmdletName} + Remove-File -Path $FullPath + } + + Write-Log -Message "Creating shortcut [$FullPath]." -Source ${CmdletName} + If ($extension -eq '.url') { [string[]]$URLFile = '[InternetShortcut]' $URLFile += "URL=$targetPath" - If ($iconIndex) { $URLFile += "IconIndex=$iconIndex" } - If ($IconLocation) { $URLFile += "IconFile=$iconLocation" } - $URLFile | Out-File -FilePath $path.FullName -Force -Encoding 'default' -ErrorAction 'Stop' - } - ElseIf (($path.FullName).ToLower().EndsWith('.lnk')) { - If (($iconLocation -and $iconIndex) -and (-not ($iconLocation.Contains(',')))) { - $iconLocation = $iconLocation + ",$iconIndex" - } + If ($IconIndex -ne $null) { $URLFile += "IconIndex=$IconIndex" } + If ($IconLocation) { $URLFile += "IconFile=$IconLocation" } + [IO.File]::WriteAllLines($FullPath,$URLFile,(new-object -TypeName Text.UTF8Encoding -ArgumentList $false)) + } Else { + $shortcut = $shell.CreateShortcut($FullPath) + ## TargetPath + $shortcut.TargetPath = $targetPath + ## Arguments + If ($arguments) { $shortcut.Arguments = $arguments } + ## Description + If ($description) { $shortcut.Description = $description } + ## Working directory + If ($workingDirectory) { $shortcut.WorkingDirectory = $workingDirectory } + ## Window Style Switch ($windowStyle) { 'Normal' { $windowStyleInt = 1 } 'Maximized' { $windowStyleInt = 3 } 'Minimized' { $windowStyleInt = 7 } Default { $windowStyleInt = 1 } } - $shortcut = $shell.CreateShortcut($path.FullName) - $shortcut.TargetPath = $targetPath - $shortcut.Arguments = $arguments - $shortcut.Description = $description - $shortcut.WorkingDirectory = $workingDirectory $shortcut.WindowStyle = $windowStyleInt - If ($hotkey) {$shortcut.Hotkey = $hotkey} - If ($iconLocation) { $shortcut.IconLocation = $iconLocation } + ## Hotkey + If ($hotkey) { $shortcut.Hotkey = $hotkey } + ## Icon + If ($IconIndex -eq $null) { + $IconIndex = 0 + } + If ($IconLocation) { $shortcut.IconLocation = $IconLocation + ",$IconIndex" } + ## Save the changes $shortcut.Save() ## Set shortcut to run program as administrator If ($RunAsAdmin) { - Write-Log -Message 'Set shortcut to run program as administrator.' -Source ${CmdletName} - $TempFileName = [IO.Path]::GetRandomFileName() - $TempFile = [IO.FileInfo][IO.Path]::Combine($Path.Directory, $TempFileName) - $Writer = New-Object -TypeName 'System.IO.FileStream' -ArgumentList ($TempFile, ([IO.FileMode]::Create)) -ErrorAction 'Stop' - $Reader = $Path.OpenRead() - While ($Reader.Position -lt $Reader.Length) { - $Byte = $Reader.ReadByte() - If ($Reader.Position -eq 22) { $Byte = 34 } - $Writer.WriteByte($Byte) - } - $Reader.Close() - $Writer.Close() - $Path.Delete() - $null = Rename-Item -Path $TempFile -NewName $Path.Name -Force -ErrorAction 'Stop' + Write-Log -Message 'Setting shortcut to run program as administrator.' -Source ${CmdletName} + [byte[]]$filebytes = [IO.FIle]::ReadAllBytes($FullPath) + $filebytes[21] = $filebytes[21] -bor 32 + [IO.FIle]::WriteAllBytes($FullPath,$filebytes) } } } Catch { - Write-Log -Message "Failed to create shortcut [$($path.FullName)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + Write-Log -Message "Failed to create shortcut [$Path]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} If (-not $ContinueOnError) { - Throw "Failed to create shortcut [$($path.FullName)]: $($_.Exception.Message)" + Throw "Failed to create shortcut [$Path]: $($_.Exception.Message)" } } } @@ -4869,69 +5077,78 @@ Function New-Shortcut { } #endregion - -#region Function Execute-ProcessAsUser -Function Execute-ProcessAsUser { +#region Function Set-Shortcut +Function Set-Shortcut { <# .SYNOPSIS - Execute a process with a logged in user account, by using a scheduled task, to provide interaction with user in the SYSTEM context. + Modifies a .lnk or .url type shortcut .DESCRIPTION - Execute a process with a logged in user account, by using a scheduled task, to provide interaction with user in the SYSTEM context. -.PARAMETER UserName - Logged in Username under which to run the process from. Default is: The active console user. If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user. + Modifies a shortcut - .lnk or .url file, with configurable options. + Only specify the parameters that you want to change. .PARAMETER Path - Path to the file being executed. -.PARAMETER Parameters - Arguments to be passed to the file being executed. -.PARAMETER SecureParameters - Hides all parameters passed to the executable from the Toolkit log file. -.PARAMETER RunLevel - Specifies the level of user rights that Task Scheduler uses to run the task. The acceptable values for this parameter are: - - HighestAvailable: Tasks run by using the highest available privileges (Admin privileges for Administrators). Default Value. - - LeastPrivilege: Tasks run by using the least-privileged user account (LUA) privileges. -.PARAMETER Wait - Wait for the process, launched by the scheduled task, to complete execution before accepting more input. Default is $false. -.PARAMETER PassThru - Returns the exit code from this function or the process launched by the scheduled task. + Path to the shortcut to be changed +.PARAMETER TargetPath + Changes target path or URL that the shortcut launches +.PARAMETER Arguments + Changes Arguments to be passed to the target path +.PARAMETER IconLocation + Changes location of the icon used for the shortcut +.PARAMETER IconIndex + Change the index of the icon. Executables, DLLs, ICO files with multiple icons need the icon index to be specified. This parameter is an Integer. The first index is 0. +.PARAMETER Description + Changes description of the shortcut .PARAMETER WorkingDirectory - Set working directory for the process. + Changes Working Directory to be used for the target path +.PARAMETER WindowStyle + Changes the Windows style of the application. Options: Normal, Maximized, Minimized, DontChange. Default is: DontChange. +.PARAMETER RunAsAdmin + Set shortcut to run program as administrator. This option will prompt user to elevate when executing shortcut. If not specified or set to $null, the flag will not be changed. +.PARAMETER Hotkey + Changes the Hotkey to launch the shortcut, e.g. "CTRL+SHIFT+F" .PARAMETER ContinueOnError - Continue if an error is encountered. Default is $true. -.EXAMPLE - Execute-ProcessAsUser -UserName 'CONTOSO\User' -Path "$PSHOME\powershell.exe" -Parameters "-Command & { & `"C:\Test\Script.ps1`"; Exit `$LastExitCode }" -Wait - Execute process under a user account by specifying a username under which to execute it. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Execute-ProcessAsUser -Path "$PSHOME\powershell.exe" -Parameters "-Command & { & `"C:\Test\Script.ps1`"; Exit `$LastExitCode }" -Wait - Execute process under a user account by using the default active logged in user that was detected when the toolkit was launched. + Set-Shortcut -Path "$envProgramData\Microsoft\Windows\Start Menu\My Shortcut.lnk" -TargetPath "$envWinDir\system32\notepad.exe" -IconLocation "$envWinDir\system32\notepad.exe" -IconIndex 0 -Description 'Notepad' -WorkingDirectory "$envHomeDrive\$envHomePath" .NOTES + Url shortcuts only support TargetPath, IconLocation and IconIndex. Other parameters are ignored. .LINK http://psappdeploytoolkit.com #> - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName="Default")] Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$UserName = $RunAsActiveUser.NTAccount, - [Parameter(Mandatory=$true)] + [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0, ParameterSetName="Default")] [ValidateNotNullorEmpty()] [string]$Path, + [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0, ParameterSetName="Pipeline")] + [ValidateNotNullorEmpty()] + [hashtable]$PathHash, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] - [string]$Parameters = '', + [string]$TargetPath, [Parameter(Mandatory=$false)] - [switch]$SecureParameters = $false, + [ValidateNotNullOrEmpty()] + [string]$Arguments, [Parameter(Mandatory=$false)] - [ValidateSet('HighestAvailable','LeastPrivilege')] - [string]$RunLevel = 'HighestAvailable', + [ValidateNotNullorEmpty()] + [string]$IconLocation, [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$Wait = $false, + [ValidateNotNullorEmpty()] + [string]$IconIndex, [Parameter(Mandatory=$false)] - [switch]$PassThru = $false, + [ValidateNotNullOrEmpty()] + [string]$Description, [Parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [string]$WorkingDirectory, [Parameter(Mandatory=$false)] + [ValidateSet('Normal','Maximized','Minimized','DontChange')] + [string]$WindowStyle='DontChange', + [Parameter(Mandatory=$false)] + [System.Nullable[bool]]$RunAsAdmin, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Hotkey, + [Parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [boolean]$ContinueOnError = $true ) @@ -4940,58 +5157,385 @@ Function Execute-ProcessAsUser { ## Get the name of this function and write header [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + If (-not $Shell) { [__comobject]$Shell = New-Object -ComObject 'WScript.Shell' -ErrorAction 'Stop' } } Process { - ## Initialize exit code variable - [int32]$executeProcessAsUserExitCode = 0 - - ## Confirm that the username field is not empty - If (-not $UserName) { - [int32]$executeProcessAsUserExitCode = 60009 - Write-Log -Message "The function [${CmdletName}] has a -UserName parameter that has an empty default value because no logged in users were detected when the toolkit was launched." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "The function [${CmdletName}] has a -UserName parameter that has an empty default value because no logged in users were detected when the toolkit was launched." + Try { + if($PsCmdlet.ParameterSetName -eq "Pipeline") { + $Path = $PathHash.Path } - Return - } - ## Confirm if the toolkit is running with administrator privileges - If (($RunLevel -eq 'HighestAvailable') -and (-not $IsAdmin)) { - [int32]$executeProcessAsUserExitCode = 60003 - Write-Log -Message "The function [${CmdletName}] requires the toolkit to be running with Administrator privileges if the [-RunLevel] parameter is set to 'HighestAvailable'." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "The function [${CmdletName}] requires the toolkit to be running with Administrator privileges if the [-RunLevel] parameter is set to 'HighestAvailable'." + If (-not (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction 'Stop')) { + Write-Log -Message "Failed to find the file [$Path]." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw + } + return } - Return - } + $extension = [IO.Path]::GetExtension($Path).ToLower() + If ((-not $extension) -or (($extension -ne '.lnk') -and ($extension -ne '.url'))) { + Write-Log -Message "Specified file [$Path] is not a valid shortcut." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw + } + return + } + # Make sure Net framework current dir is synced with powershell cwd + [IO.Directory]::SetCurrentDirectory((Get-Location)) + Write-Log -Message "Changing shortcut [$Path]." -Source ${CmdletName} + If ($extension -eq '.url') { + [string[]]$URLFile = [IO.File]::ReadAllLines($Path) + for($i = 0; $i -lt $URLFile.Length; $i++) { + $URLFile[$i] = $URLFile[$i].TrimStart() + if($URLFile[$i].StartsWith('URL=') -and $targetPath) { $URLFile[$i] = "URL=$targetPath" } + elseif($URLFile[$i].StartsWith('IconIndex=') -and ($IconIndex -ne $null)) { $URLFile[$i] = "IconIndex=$IconIndex" } + elseif($URLFile[$i].StartsWith('IconFile=') -and $IconLocation) { $URLFile[$i] = "IconFile=$IconLocation" } + } + [IO.File]::WriteAllLines($Path,$URLFile,(new-object -TypeName Text.UTF8Encoding -ArgumentList $false)) + } Else { + $shortcut = $shell.CreateShortcut($Path) + ## TargetPath + If ($targetPath) { $shortcut.TargetPath = $targetPath } + ## Arguments + If ($arguments) { $shortcut.Arguments = $arguments } + ## Description + If ($description) { $shortcut.Description = $description } + ## Working directory + If ($workingDirectory) { $shortcut.WorkingDirectory = $workingDirectory } + ## Window Style + Switch ($windowStyle) { + 'Normal' { $windowStyleInt = 1 } + 'Maximized' { $windowStyleInt = 3 } + 'Minimized' { $windowStyleInt = 7 } + 'DontChange' { $windowStyleInt = 0 } + Default { $windowStyleInt = 1 } + } + if ($windowStyleInt -ne 0) { + $shortcut.WindowStyle = $windowStyleInt + } + ## Hotkey + If ($hotkey) { $shortcut.Hotkey = $hotkey } + ## Icon + # Retrieve previous value and split the path from the index + [string[]]$Split = $shortcut.IconLocation.Split(',') + $TempIconLocation = $Split[0] + $TempIconIndex = $Split[1] + # Check whether a new icon path was specified + If ($IconLocation) { + # New icon path was specified. Check whether new icon index was also specified + If ($IconIndex -ne $null) { + # Create new icon path from new icon path and new icon index + $IconLocation = $IconLocation + ",$IconIndex" + } else { + # No new icon index was specified as a parameter. We will keep the old one + $IconLocation = $IconLocation + ",$TempIconIndex" + } + } elseif ($IconIndex -ne $null) { + # New icon index was specified, but not the icon location. Append it to the icon path from the shortcut + $IconLocation = $TempIconLocation + ",$IconIndex" + } + If ($IconLocation) { $shortcut.IconLocation = $IconLocation } + ## Save the changes + $shortcut.Save() - ## Check whether the specified Working Directory exists - If ($WorkingDirectory -and (-not (Test-Path -LiteralPath $WorkingDirectory -PathType 'Container'))) { - Write-Log -Message "The specified working directory does not exist or is not a directory. The scheduled task might not work as expected." -Severity 2 -Source ${CmdletName} + ## Set shortcut to run program as administrator + If ($RunAsAdmin -eq $true) { + Write-Log -Message 'Setting shortcut to run program as administrator.' -Source ${CmdletName} + [byte[]]$filebytes = [IO.FIle]::ReadAllBytes($Path) + $filebytes[21] = $filebytes[21] -bor 32 + [IO.FIle]::WriteAllBytes($Path,$filebytes) + } elseif ($RunAsAdmin -eq $false) { + [byte[]]$filebytes = [IO.FIle]::ReadAllBytes($Path) + Write-Log -Message 'Setting shortcut to not run program as administrator.' -Source ${CmdletName} + $filebytes[21] = $filebytes[21] -band (-bnot 32) + [IO.FIle]::WriteAllBytes($Path,$filebytes) + } + } } - - ## Build the scheduled task XML name - [string]$schTaskName = "$appDeployToolkitName-ExecuteAsUser" - - ## Create the temporary App Deploy Toolkit files folder if it doesn't already exist - If (-not (Test-Path -LiteralPath $dirAppDeployTemp -PathType 'Container')) { - New-Item -Path $dirAppDeployTemp -ItemType 'Directory' -Force -ErrorAction 'Stop' + Catch { + Write-Log -Message "Failed to change the shortcut [$Path]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to change the shortcut [$Path]: $($_.Exception.Message)" + } } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } +} +#endregion - ## If PowerShell.exe is being launched, then create a VBScript to launch PowerShell so that we can suppress the console window that flashes otherwise - If (($Path -eq 'PowerShell.exe') -or ((Split-Path -Path $Path -Leaf) -eq 'PowerShell.exe')) { - # Permit inclusion of double quotes in parameters - If ($($Parameters.Substring($Parameters.Length - 1)) -eq '"') { - [string]$executeProcessAsUserParametersVBS = 'chr(34) & ' + "`"$($Path)`"" + ' & chr(34) & ' + '" ' + ($Parameters -replace "`r`n", ';' -replace "`n", ';' -replace '"', "`" & chr(34) & `"" -replace ' & chr\(34\) & "$', '') + ' & chr(34)' } - Else { +#region Function Get-Shortcut +Function Get-Shortcut { +<# +.SYNOPSIS + Get information from a new .lnk or .url type shortcut +.DESCRIPTION + Get information from a new .lnk or .url type shortcut. Returns a hashtable. +.PARAMETER Path + Path to the shortcut to get information from +.PARAMETER ContinueOnError + Continue if an error is encountered. Default is: $true. +.EXAMPLE + Get-Shortcut -Path "$envProgramData\Microsoft\Windows\Start Menu\My Shortcut.lnk" +.NOTES + Url shortcuts only support TargetPath, IconLocation and IconIndex. +.LINK + http://psappdeploytoolkit.com +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true, Position=0)] + [ValidateNotNullorEmpty()] + [string]$Path, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + If (-not $Shell) { [__comobject]$Shell = New-Object -ComObject 'WScript.Shell' -ErrorAction 'Stop' } + } + Process { + Try { + $extension = [IO.Path]::GetExtension($Path).ToLower() + If ((-not $extension) -or (($extension -ne '.lnk') -and ($extension -ne '.url'))) { + Write-Log -Message "Specified file [$Path] does not have a valid shortcut extension: .url .lnk" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw + } + return + } + Try { + # Make sure Net framework current dir is synced with powershell cwd + [IO.Directory]::SetCurrentDirectory((Get-Location)) + # Get full path + [string]$FullPath = [IO.Path]::GetFullPath($Path) + } + Catch { + Write-Log -Message "Specified path [$Path] is not valid." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw + } + return + } + + $Output = @{ Path = $FullPath } + If ($extension -eq '.url') { + [string[]]$URLFile = [IO.File]::ReadAllLines($Path) + for($i = 0; $i -lt $URLFile.Length; $i++) { + $URLFile[$i] = $URLFile[$i].TrimStart() + if($URLFile[$i].StartsWith('URL=')) { $Output.TargetPath = $URLFile[$i].Replace('URL=','') } + elseif($URLFile[$i].StartsWith('IconIndex=')) { $Output.IconIndex = $URLFile[$i].Replace('IconIndex=','') } + elseif($URLFile[$i].StartsWith('IconFile=')) { $Output.IconLocation = $URLFile[$i].Replace('IconFile=','') } + } + } Else { + $shortcut = $shell.CreateShortcut($FullPath) + ## TargetPath + $Output.TargetPath = $shortcut.TargetPath + ## Arguments + $Output.Arguments = $shortcut.Arguments + ## Description + $Output.Description = $shortcut.Description + ## Working directory + $Output.WorkingDirectory = $shortcut.WorkingDirectory + ## Window Style + Switch ($shortcut.WindowStyle) { + 1 { $Output.WindowStyle = 'Normal'} + 3 { $Output.WindowStyle = 'Maximized'} + 7 { $Output.WindowStyle = 'Minimized'} + Default { $Output.WindowStyle = 'Normal'} + } + ## Hotkey + $Output.Hotkey = $shortcut.Hotkey + ## Icon + [string[]]$Split = $shortcut.IconLocation.Split(',') + $Output.IconLocation = $Split[0] + $Output.IconIndex = $Split[1] + ## Remove the variable + $shortcut = $null + ## Run as admin + [byte[]]$filebytes = [IO.FIle]::ReadAllBytes($FullPath) + if ($filebytes[21] -band 32) { + $Output.RunAsAdmin = $true + } + else { + $Output.RunAsAdmin = $false + } + } + Write-Output $Output + } + Catch { + Write-Log -Message "Failed to read the shortcut [$Path]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to read the shortcut [$Path]: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } +} +#endregion + +#region Function Execute-ProcessAsUser +Function Execute-ProcessAsUser { +<# +.SYNOPSIS + Execute a process with a logged in user account, by using a scheduled task, to provide interaction with user in the SYSTEM context. +.DESCRIPTION + Execute a process with a logged in user account, by using a scheduled task, to provide interaction with user in the SYSTEM context. +.PARAMETER UserName + Logged in Username under which to run the process from. Default is: The active console user. If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user. +.PARAMETER Path + Path to the file being executed. +.PARAMETER Parameters + Arguments to be passed to the file being executed. +.PARAMETER SecureParameters + Hides all parameters passed to the executable from the Toolkit log file. +.PARAMETER RunLevel + Specifies the level of user rights that Task Scheduler uses to run the task. The acceptable values for this parameter are: + - HighestAvailable: Tasks run by using the highest available privileges (Admin privileges for Administrators). Default Value. + - LeastPrivilege: Tasks run by using the least-privileged user account (LUA) privileges. +.PARAMETER Wait + Wait for the process, launched by the scheduled task, to complete execution before accepting more input. Default is $false. +.PARAMETER PassThru + Returns the exit code from this function or the process launched by the scheduled task. +.PARAMETER WorkingDirectory + Set working directory for the process. +.PARAMETER ContinueOnError + Continue if an error is encountered. Default is $true. +.EXAMPLE + Execute-ProcessAsUser -UserName 'CONTOSO\User' -Path "$PSHOME\powershell.exe" -Parameters "-Command & { & `"C:\Test\Script.ps1`"; Exit `$LastExitCode }" -Wait + Execute process under a user account by specifying a username under which to execute it. +.EXAMPLE + Execute-ProcessAsUser -Path "$PSHOME\powershell.exe" -Parameters "-Command & { & `"C:\Test\Script.ps1`"; Exit `$LastExitCode }" -Wait + Execute process under a user account by using the default active logged in user that was detected when the toolkit was launched. +.NOTES +.LINK + http://psappdeploytoolkit.com +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$UserName = $RunAsActiveUser.NTAccount, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Path, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Parameters = '', + [Parameter(Mandatory=$false)] + [switch]$SecureParameters = $false, + [Parameter(Mandatory=$false)] + [ValidateSet('HighestAvailable','LeastPrivilege')] + [string]$RunLevel = 'HighestAvailable', + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$Wait = $false, + [Parameter(Mandatory=$false)] + [switch]$PassThru = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$WorkingDirectory, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + [string]$executeAsUserTempPath = Join-Path -Path $dirAppDeployTemp -ChildPath 'ExecuteAsUser' + } + Process { + ## Initialize exit code variable + [int32]$executeProcessAsUserExitCode = 0 + + ## Confirm that the username field is not empty + If (-not $UserName) { + [int32]$executeProcessAsUserExitCode = 60009 + Write-Log -Message "The function [${CmdletName}] has a -UserName parameter that has an empty default value because no logged in users were detected when the toolkit was launched." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "The function [${CmdletName}] has a -UserName parameter that has an empty default value because no logged in users were detected when the toolkit was launched." + } + Return + } + + ## Confirm if the toolkit is running with administrator privileges + If (($RunLevel -eq 'HighestAvailable') -and (-not $IsAdmin)) { + [int32]$executeProcessAsUserExitCode = 60003 + Write-Log -Message "The function [${CmdletName}] requires the toolkit to be running with Administrator privileges if the [-RunLevel] parameter is set to 'HighestAvailable'." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "The function [${CmdletName}] requires the toolkit to be running with Administrator privileges if the [-RunLevel] parameter is set to 'HighestAvailable'." + } + Return + } + + ## Check whether the specified Working Directory exists + If ($WorkingDirectory -and (-not (Test-Path -LiteralPath $WorkingDirectory -PathType 'Container'))) { + Write-Log -Message "The specified working directory does not exist or is not a directory. The scheduled task might not work as expected." -Severity 2 -Source ${CmdletName} + } + + ## Build the scheduled task XML name + [string]$schTaskName = "$appDeployToolkitName-ExecuteAsUser" + + ## Remove and recreate the temporary folder + If (Test-Path -LiteralPath $executeAsUserTempPath -PathType 'Container') { + Write-Log -Message "Previous [$executeAsUserTempPath] found. Attempting removal." -Source ${CmdletName} + Remove-Folder -Path $executeAsUserTempPath + } + Write-Log -Message "Creating [$executeAsUserTempPath]." -Source ${CmdletName} + Try { + $null = New-Item -Path $executeAsUserTempPath -ItemType 'Directory' -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Unable to create [$executeAsUserTempPath]. Possible attempt to gain elevated rights." -Source ${CmdletName} -Severity 2 + } + + ## Escape XML characters + $EscapedPath = [System.Security.SecurityElement]::Escape($Path) + $EscapedParameters = [System.Security.SecurityElement]::Escape($Parameters) + + ## If PowerShell.exe is being launched, then create a VBScript to launch PowerShell so that we can suppress the console window that flashes otherwise + If (((Split-Path -Path $Path -Leaf) -like 'PowerShell*') -or ((Split-Path -Path $Path -Leaf) -like 'cmd*')) { + If ($SecureParameters) { + Write-Log -Message "Preparing a vbs script that will start [$Path] (Parameters Hidden) as the logged-on user [$userName] silently..." -Source ${CmdletName} + } + Else { + Write-Log -Message "Preparing a vbs script that will start [$Path $Parameters] as the logged-on user [$userName] silently..." -Source ${CmdletName} + } + # Permit inclusion of double quotes in parameters + $QuotesIndex = $Parameters.Length - 1 + If ($QuotesIndex -lt 0) { + $QuotesIndex = 0 + } + + If ($($Parameters.Substring($QuotesIndex)) -eq '"') { + [string]$executeProcessAsUserParametersVBS = 'chr(34) & ' + "`"$($Path)`"" + ' & chr(34) & ' + '" ' + ($Parameters -replace "`r`n", ';' -replace "`n", ';' -replace '"', "`" & chr(34) & `"" -replace ' & chr\(34\) & "$', '') + ' & chr(34)' } + Else { [string]$executeProcessAsUserParametersVBS = 'chr(34) & ' + "`"$($Path)`"" + ' & chr(34) & ' + '" ' + ($Parameters -replace "`r`n", ';' -replace "`n", ';' -replace '"', "`" & chr(34) & `"" -replace ' & chr\(34\) & "$','') + '"' } [string[]]$executeProcessAsUserScript = "strCommand = $executeProcessAsUserParametersVBS" $executeProcessAsUserScript += 'set oWShell = CreateObject("WScript.Shell")' $executeProcessAsUserScript += 'intReturn = oWShell.Run(strCommand, 0, true)' $executeProcessAsUserScript += 'WScript.Quit intReturn' - $executeProcessAsUserScript | Out-File -FilePath "$dirAppDeployTemp\$($schTaskName).vbs" -Force -Encoding 'default' -ErrorAction 'SilentlyContinue' - $Path = 'wscript.exe' - $Parameters = "`"$dirAppDeployTemp\$($schTaskName).vbs`"" + $executeProcessAsUserScript | Out-File -FilePath "$executeAsUserTempPath\$($schTaskName).vbs" -Force -Encoding 'default' -ErrorAction 'SilentlyContinue' + $Path = "$envWinDir\System32\wscript.exe" + $Parameters = "`"$executeAsUserTempPath\$($schTaskName).vbs`"" + + try { + Set-ItemPermission -Path "$executeAsUserTempPath\$schTaskName.vbs" -User $UserName -Permission 'Read' + } + catch { + Write-Log -Message "Failed to set read permissions on path [$executeAsUserTempPath\$schTaskName.vbs]. The function might not be able to work correctly." -Source ${CmdletName} -Severity 2 + } } ## Prepare working directory insert @@ -5026,8 +5570,8 @@ Function Execute-ProcessAsUser { - $Path - $Parameters$WorkingDirectoryInsert + $EscapedPath + $EscapedParameters$WorkingDirectoryInsert @@ -5044,6 +5588,7 @@ Function Execute-ProcessAsUser { # Specify the filename to export the XML to [string]$xmlSchTaskFilePath = "$dirAppDeployTemp\$schTaskName.xml" [string]$xmlSchTask | Out-File -FilePath $xmlSchTaskFilePath -Force -ErrorAction 'Stop' + Set-ItemPermission -Path $xmlSchTaskFilePath -User $UserName -Permission 'Read' } Catch { [int32]$executeProcessAsUserExitCode = 60007 @@ -5057,16 +5602,16 @@ Function Execute-ProcessAsUser { ## Create Scheduled Task to run the process with a logged-on user account If ($Parameters) { If ($SecureParameters) { - Write-Log -Message "Create scheduled task to run the process [$Path] (Parameters Hidden) as the logged-on user [$userName]..." -Source ${CmdletName} + Write-Log -Message "Creating scheduled task to run the process [$Path] (Parameters Hidden) as the logged-on user [$userName]..." -Source ${CmdletName} } Else { - Write-Log -Message "Create scheduled task to run the process [$Path $Parameters] as the logged-on user [$userName]..." -Source ${CmdletName} + Write-Log -Message "Creating scheduled task to run the process [$Path $Parameters] as the logged-on user [$userName]..." -Source ${CmdletName} } } Else { - Write-Log -Message "Create scheduled task to run the process [$Path] as the logged-on user [$userName]..." -Source ${CmdletName} + Write-Log -Message "Creating scheduled task to run the process [$Path] as the logged-on user [$userName]..." -Source ${CmdletName} } - [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/create /f /tn $schTaskName /xml `"$xmlSchTaskFilePath`"" -WindowStyle 'Hidden' -CreateNoWindow -PassThru + [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/create /f /tn $schTaskName /xml `"$xmlSchTaskFilePath`"" -WindowStyle 'Hidden' -CreateNoWindow -PassThru -ExitOnProcessFailure $false If ($schTaskResult.ExitCode -ne 0) { [int32]$executeProcessAsUserExitCode = $schTaskResult.ExitCode Write-Log -Message "Failed to create the scheduled task by importing the scheduled task XML file [$xmlSchTaskFilePath]." -Severity 3 -Source ${CmdletName} @@ -5088,13 +5633,13 @@ Function Execute-ProcessAsUser { Else { Write-Log -Message "Trigger execution of scheduled task with command [$Path] as the logged-on user [$userName]..." -Source ${CmdletName} } - [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/run /i /tn $schTaskName" -WindowStyle 'Hidden' -CreateNoWindow -Passthru + [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/run /i /tn $schTaskName" -WindowStyle 'Hidden' -CreateNoWindow -Passthru -ExitOnProcessFailure $false If ($schTaskResult.ExitCode -ne 0) { [int32]$executeProcessAsUserExitCode = $schTaskResult.ExitCode Write-Log -Message "Failed to trigger scheduled task [$schTaskName]." -Severity 3 -Source ${CmdletName} # Delete Scheduled Task Write-Log -Message 'Delete the scheduled task which did not trigger.' -Source ${CmdletName} - Execute-Process -Path $exeSchTasks -Parameters "/delete /tn $schTaskName /f" -WindowStyle 'Hidden' -CreateNoWindow -IgnoreExitCodes "*" + Execute-Process -Path $exeSchTasks -Parameters "/delete /tn $schTaskName /f" -WindowStyle 'Hidden' -CreateNoWindow -ExitOnProcessFailure $false If (-not $ContinueOnError) { Throw "Failed to trigger scheduled task [$schTaskName]." } @@ -5128,11 +5673,11 @@ Function Execute-ProcessAsUser { } #Windows Task Scheduler 1.0 Else { - While ((($exeSchTasksResult = & $exeSchTasks /query /TN $schTaskName /V /FO CSV) | ConvertFrom-CSV | Select-Object -ExpandProperty 'Status' | Select-Object -First 1) -eq 'Running') { + While ((($exeSchTasksResult = & $exeSchTasks /query /TN $schTaskName /V /FO CSV) | ConvertFrom-CSV | Select-Object -ExpandProperty 'Status' -First 1) -eq 'Running') { Start-Sleep -Seconds 5 } # Get the exit code from the process launched by the scheduled task - [int32]$executeProcessAsUserExitCode = ($exeSchTasksResult = & $exeSchTasks /query /TN $schTaskName /V /FO CSV) | ConvertFrom-CSV | Select-Object -ExpandProperty 'Last Result' | Select-Object -First 1 + [int32]$executeProcessAsUserExitCode = ($exeSchTasksResult = & $exeSchTasks /query /TN $schTaskName /V /FO CSV) | ConvertFrom-CSV | Select-Object -ExpandProperty 'Last Result' -First 1 } Write-Log -Message "Exit code from process launched by scheduled task [$executeProcessAsUserExitCode]." -Source ${CmdletName} } @@ -5148,6 +5693,16 @@ Function Execute-ProcessAsUser { Catch { Write-Log -Message "Failed to delete scheduled task [$schTaskName]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} } + + ## Remove the XML scheduled task file + If (Test-Path -LiteralPath $xmlSchTaskFilePath -PathType 'Leaf') { + Remove-File -Path $xmlSchTaskFilePath + } + + ## Remove the temporary folder + If (Test-Path -LiteralPath $executeAsUserTempPath -PathType 'Container') { + Remove-Folder -Path $executeAsUserTempPath + } } End { If ($PassThru) { Write-Output -InputObject $executeProcessAsUserExitCode } @@ -5170,6 +5725,7 @@ Function Update-Desktop { .EXAMPLE Update-Desktop .NOTES + This function has an alias: Refresh-Desktop .LINK http://psappdeploytoolkit.com #> @@ -5220,6 +5776,7 @@ Function Update-SessionEnvironmentVariables { .EXAMPLE Update-SessionEnvironmentVariables .NOTES + This function has an alias: Refresh-SessionEnvironmentVariables .LINK http://psappdeploytoolkit.com #> @@ -5260,10 +5817,10 @@ Function Update-SessionEnvironmentVariables { [string]$UserEnvironmentVars = "Registry::HKEY_USERS\$CurrentUserEnvironmentSID\Environment" ## Update all session environment variables. Ordering is important here: $UserEnvironmentVars comes second so that we can override $MachineEnvironmentVars. - $MachineEnvironmentVars, $UserEnvironmentVars | Get-Item | Where-Object { $_ } | ForEach-Object { $envRegPath = $_.PSPath; $_ | Select-Object -ExpandProperty 'Property' | ForEach-Object { Set-Item -LiteralPath "env:$($_)" -Value (Get-ItemProperty -LiteralPath $envRegPath -Name $_).$_ } } + $MachineEnvironmentVars, $UserEnvironmentVars | Get-Item | ForEach-Object { if($_){$envRegPath = $_.PSPath; $_.Property | ForEach-Object { Set-Item -LiteralPath "env:$($_)" -Value (Get-ItemProperty -LiteralPath $envRegPath -Name $_).$_ } } } ## Set PATH environment variable separately because it is a combination of the user and machine environment variables - [string[]]$PathFolders = 'Machine', 'User' | ForEach-Object { (& $GetEnvironmentVar -Key 'PATH' -Scope $_) } | Where-Object { $_ } | ForEach-Object { $_.Trim(';') } | ForEach-Object { $_.Split(';') } | ForEach-Object { $_.Trim() } | ForEach-Object { $_.Trim('"') } | Select-Object -Unique + [string[]]$PathFolders = 'Machine', 'User' | ForEach-Object { $EachPathFolder = (& $GetEnvironmentVar -Key 'PATH' -Scope $_); if($EachPathFolder){ $EachPathFolder.Trim(';').Split(';').Trim().Trim('"') } } | Select-Object -Unique $env:PATH = $PathFolders -join ';' } Catch { @@ -5281,8 +5838,8 @@ Set-Alias -Name 'Refresh-SessionEnvironmentVariables' -Value 'Update-SessionEnvi #endregion -#region Function Get-ScheduledTask -Function Get-ScheduledTask { +#region Function Get-SchedulerTask +Function Get-SchedulerTask { <# .SYNOPSIS Retrieve all details for scheduled tasks on the local computer. @@ -5293,15 +5850,16 @@ Function Get-ScheduledTask { .PARAMETER ContinueOnError Continue if an error is encountered. Default: $true. .EXAMPLE - Get-ScheduledTask + Get-SchedulerTask To display a list of all scheduled task properties. .EXAMPLE - Get-ScheduledTask | Out-GridView + Get-SchedulerTask | Out-GridView To display a grid view of all scheduled task properties. .EXAMPLE - Get-ScheduledTask | Select-Object -Property TaskName + Get-SchedulerTask | Select-Object -Property TaskName To display a list of all scheduled task names. .NOTES + This function has an alias: Get-ScheduledTask if Get-ScheduledTask is not defined .LINK http://psappdeploytoolkit.com #> @@ -5320,7 +5878,6 @@ Function Get-ScheduledTask { [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - If (-not $exeSchTasks) { [string]$exeSchTasks = "$env:WINDIR\system32\schtasks.exe" } [psobject[]]$ScheduledTasks = @() } Process { @@ -5361,6 +5918,10 @@ Function Get-ScheduledTask { Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer } } +# If Get-ScheduledTask doesn't exist, add alias Get-ScheduledTask +If (-not (Get-Command -Name "Get-ScheduledTask" -ErrorAction SilentlyContinue)) { + New-Alias -Name "Get-ScheduledTask" -Value "Get-SchedulerTask" +} #endregion @@ -5405,7 +5966,8 @@ Function Block-AppExecution { [char[]]$invalidScheduledTaskChars = '$', '!', '''', '"', '(', ')', ';', '\', '`', '*', '?', '{', '}', '[', ']', '<', '>', '|', '&', '%', '#', '~', '@', ' ' [string]$SchInstallName = $installName ForEach ($invalidChar in $invalidScheduledTaskChars) { [string]$SchInstallName = $SchInstallName -replace [regex]::Escape($invalidChar),'' } - [string]$schTaskUnblockAppsCommand += "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$dirAppDeployTemp\$scriptFileName`" -CleanupBlockedApps -ReferredInstallName `"$SchInstallName`" -ReferredInstallTitle `"$installTitle`" -ReferredLogName `"$logName`" -AsyncToolkitLaunch" + [string]$blockExecutionTempPath = Join-Path -Path $dirAppDeployTemp -ChildPath 'BlockExecution' + [string]$schTaskUnblockAppsCommand += "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$blockExecutionTempPath\$scriptFileName`" -CleanupBlockedApps -ReferredInstallName `"$SchInstallName`" -ReferredInstallTitle `"$installTitle`" -ReferredLogName `"$logName`" -AsyncToolkitLaunch" ## Specify the scheduled task configuration in XML format [string]$xmlUnblockAppsSchTask = @" @@ -5442,7 +6004,7 @@ Function Block-AppExecution { - powershell.exe + $PSHome\powershell.exe $schTaskUnblockAppsCommand @@ -5450,9 +6012,14 @@ Function Block-AppExecution { "@ } Process { + ## Bypass if no Admin rights + If ($configToolkitRequireAdmin -eq $false) { + Write-Log -Message "Bypassing Function [${CmdletName}], because [Require Admin: $configToolkitRequireAdmin]." -Source ${CmdletName} + Return + } ## Bypass if in NonInteractive mode If ($deployModeNonInteractive) { - Write-Log -Message "Bypassing Function [${CmdletName}] [Mode: $deployMode]." -Source ${CmdletName} + Write-Log -Message "Bypassing Function [${CmdletName}], because [Mode: $deployMode]." -Source ${CmdletName} Return } @@ -5462,30 +6029,47 @@ Function Block-AppExecution { If (Test-Path -LiteralPath "$configToolkitTempPath\PSAppDeployToolkit" -PathType 'Leaf' -ErrorAction 'SilentlyContinue') { $null = Remove-Item -LiteralPath "$configToolkitTempPath\PSAppDeployToolkit" -Force -ErrorAction 'SilentlyContinue' } - ## Create Temporary directory (if required) and copy Toolkit so it can be called by scheduled task later if required - If (-not (Test-Path -LiteralPath $dirAppDeployTemp -PathType 'Container' -ErrorAction 'SilentlyContinue')) { - $null = New-Item -Path $dirAppDeployTemp -ItemType 'Directory' -ErrorAction 'SilentlyContinue' + + If (Test-Path -LiteralPath $blockExecutionTempPath -PathType 'Container') { + Remove-Folder -Path $blockExecutionTempPath + } + + Try { + $null = New-Item -Path $blockExecutionTempPath -ItemType 'Directory' -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Unable to create [$blockExecutionTempPath]. Possible attempt to gain elevated rights." -Source ${CmdletName} } - Copy-Item -Path "$scriptRoot\*.*" -Destination $dirAppDeployTemp -Exclude 'thumbs.db' -Force -Recurse -ErrorAction 'SilentlyContinue' + Copy-Item -Path "$scriptRoot\*.*" -Destination $blockExecutionTempPath -Exclude 'thumbs.db' -Force -Recurse -ErrorAction 'SilentlyContinue' ## Build the debugger block value script - [string]$debuggerBlockMessageCmd = "`"powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `" & chr(34) & `"$dirAppDeployTemp\$scriptFileName`" & chr(34) & `" -ShowBlockedAppDialog -AsyncToolkitLaunch -ReferredInstallTitle `" & chr(34) & `"$installTitle`" & chr(34)" + [string]$debuggerBlockMessageCmd = "`"$PSHome\powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `" & chr(34) & `"$blockExecutionTempPath\$scriptFileName`" & chr(34) & `" -ShowBlockedAppDialog -AsyncToolkitLaunch -ReferredInstallTitle `" & chr(34) & `"$installTitle`" & chr(34)" [string[]]$debuggerBlockScript = "strCommand = $debuggerBlockMessageCmd" $debuggerBlockScript += 'set oWShell = CreateObject("WScript.Shell")' $debuggerBlockScript += 'oWShell.Run strCommand, 0, false' - $debuggerBlockScript | Out-File -FilePath "$dirAppDeployTemp\AppDeployToolkit_BlockAppExecutionMessage.vbs" -Force -Encoding 'default' -ErrorAction 'SilentlyContinue' - [string]$debuggerBlockValue = "wscript.exe `"$dirAppDeployTemp\AppDeployToolkit_BlockAppExecutionMessage.vbs`"" + $debuggerBlockScript | Out-File -FilePath "$blockExecutionTempPath\AppDeployToolkit_BlockAppExecutionMessage.vbs" -Force -Encoding 'default' -ErrorAction 'SilentlyContinue' + [string]$debuggerBlockValue = "$envWinDir\System32\wscript.exe `"$blockExecutionTempPath\AppDeployToolkit_BlockAppExecutionMessage.vbs`"" + ## Set contents to be readable for all users (BUILTIN\USERS) + try { + $Users = ConvertTo-NTAccountOrSID -SID "S-1-5-32-545" + Set-ItemPermission -Path $blockExecutionTempPath -User $Users -Permission 'Read' -Inheritance "ObjectInherit","ContainerInherit" + } + catch { + Write-Log -Message "Failed to set read permissions on path [$blockExecutionTempPath]. The function might not be able to work correctly." -Source ${CmdletName} -Severity 2 + } + ## Create a scheduled task to run on startup to call this script and clean up blocked applications in case the installation is interrupted, e.g. user shuts down during installation" Write-Log -Message 'Create scheduled task to cleanup blocked applications in case installation is interrupted.' -Source ${CmdletName} - If (Get-ScheduledTask -ContinueOnError $true | Select-Object -Property 'TaskName' | Where-Object { $_.TaskName -eq "\$schTaskBlockedAppsName" }) { + If (Get-SchedulerTask -ContinueOnError $true | ForEach-Object { if($_.TaskName -eq "\$schTaskBlockedAppsName") {$_.TaskName} }) { Write-Log -Message "Scheduled task [$schTaskBlockedAppsName] already exists." -Source ${CmdletName} } Else { ## Export the scheduled task XML to file Try { - # Specify the filename to export the XML to + ## Specify the filename to export the XML to + ## XML does not need to be user readable to stays in protected TEMP folder [string]$xmlSchTaskFilePath = "$dirAppDeployTemp\SchTaskUnBlockApps.xml" [string]$xmlUnblockAppsSchTask | Out-File -FilePath $xmlSchTaskFilePath -Force -ErrorAction 'Stop' } @@ -5495,7 +6079,7 @@ Function Block-AppExecution { } ## Import the Scheduled Task XML file to create the Scheduled Task - [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/create /f /tn $schTaskBlockedAppsName /xml `"$xmlSchTaskFilePath`"" -WindowStyle 'Hidden' -CreateNoWindow -PassThru + [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/create /f /tn $schTaskBlockedAppsName /xml `"$xmlSchTaskFilePath`"" -WindowStyle 'Hidden' -CreateNoWindow -PassThru -ExitOnProcessFailure $false If ($schTaskResult.ExitCode -ne 0) { Write-Log -Message "Failed to create the scheduled task [$schTaskBlockedAppsName] by importing the scheduled task XML file [$xmlSchTaskFilePath]." -Severity 3 -Source ${CmdletName} Return @@ -5544,9 +6128,14 @@ Function Unblock-AppExecution { Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header } Process { + ## Bypass if no Admin rights + If ($configToolkitRequireAdmin -eq $false) { + Write-Log -Message "Bypassing Function [${CmdletName}], because [Require Admin: $configToolkitRequireAdmin]." -Source ${CmdletName} + Return + } ## Bypass if in NonInteractive mode If ($deployModeNonInteractive) { - Write-Log -Message "Bypassing Function [${CmdletName}] [Mode: $deployMode]." -Source ${CmdletName} + Write-Log -Message "Bypassing Function [${CmdletName}], because [Mode: $deployMode]." -Source ${CmdletName} Return } @@ -5567,7 +6156,7 @@ Function Unblock-AppExecution { ## Remove the scheduled task if it exists [string]$schTaskBlockedAppsName = $installName + '_BlockedApps' Try { - If (Get-ScheduledTask -ContinueOnError $true | Select-Object -Property 'TaskName' | Where-Object { $_.TaskName -eq "\$schTaskBlockedAppsName" }) { + If (Get-SchedulerTask -ContinueOnError $true | ForEach-Object { if($_.TaskName -eq "\$schTaskBlockedAppsName") {$_.TaskName} }) { Write-Log -Message "Delete Scheduled Task [$schTaskBlockedAppsName]." -Source ${CmdletName} Execute-Process -Path $exeSchTasks -Parameters "/Delete /TN $schTaskBlockedAppsName /F" } @@ -5575,6 +6164,18 @@ Function Unblock-AppExecution { Catch { Write-Log -Message "Error retrieving/deleting Scheduled Task.`n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} } + + ## Remove BlockAppExecution Schedule Task XML file + [string]$xmlSchTaskFilePath = "$dirAppDeployTemp\SchTaskUnBlockApps.xml" + If (Test-Path -LiteralPath $xmlSchTaskFilePath) { + Remove-Item -Path $xmlSchTaskFilePath + } + + ## Remove BlockAppExection Temporary directory + [string]$blockExecutionTempPath = Join-Path -Path $dirAppDeployTemp -ChildPath 'BlockExecution' + If (Test-Path -LiteralPath $blockExecutionTempPath -PathType 'Container') { + Remove-Folder -Path $blockExecutionTempPath + } } End { Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer @@ -5731,9 +6332,11 @@ Function Get-RunningProcesses { .DESCRIPTION Gets the processes that are running from a custom list of process objects and also adds a property called ProcessDescription. .PARAMETER ProcessObjects - Custom object containing the process objects to search for. + Custom object containing the process objects to search for. If not supplied, the function just returns $null +.PARAMETER DisableLogging + Disables function logging .EXAMPLE - Get-RunningProcesses + Get-RunningProcesses -ProcessObjects $ProcessObjects .NOTES This is an internal script function and should typically not be called directly. .LINK @@ -5753,49 +6356,50 @@ Function Get-RunningProcesses { Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header } Process { - If ($processObjects) { - [string]$runningAppsCheck = ($processObjects | ForEach-Object { $_.ProcessName }) -join ',' + If ($processObjects -and $processObjects[0].ProcessName) { + [string]$runningAppsCheck = $processObjects.ProcessName -join ',' If (-not($DisableLogging)) { - Write-Log -Message "Check for running application(s) [$runningAppsCheck]..." -Source ${CmdletName} - } - ## Create an array of process names to search for - [string[]]$processNames = $processObjects | ForEach-Object { $_.ProcessName } + Write-Log -Message "Check for running applications: [$runningAppsCheck]" -Source ${CmdletName} + } + ## Prepare a filter for Where-Object + [scriptblock]$whereObjectFilter = { + ForEach ($processObject in $processObjects) { + If ($_.ProcessName -ieq $processObject.ProcessName) { + If ($processObject.ProcessDescription) { + # The description of the process provided as a Parameter to the function, e.g. -ProcessName "winword=Microsoft Office Word". + Add-Member -InputObject $_ -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $processObject.ProcessDescription -Force -PassThru -ErrorAction 'SilentlyContinue' + } + ElseIf ($_.Description) { + # If the process already has a description field specified, then use it + Add-Member -InputObject $_ -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $_.Description -Force -PassThru -ErrorAction 'SilentlyContinue' + } + Else { + # Fall back on the process name if no description is provided by the process or as a parameter to the function + Add-Member -InputObject $_ -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $_.ProcessName -Force -PassThru -ErrorAction 'SilentlyContinue' + } + Write-Output $true + return; + } + } + Write-Output $false + return; + } ## Get all running processes and escape special characters. Match against the process names to search for to find running processes. - [Diagnostics.Process[]]$runningProcesses = Get-Process | Where-Object { $processNames -contains $_.ProcessName } | Sort-Object Name -Unique + [Diagnostics.Process[]]$runningProcesses = Get-Process | Where-Object -FilterScript $whereObjectFilter | Sort-Object ProcessName - If ($runningProcesses) { - [string]$runningProcessList = ($runningProcesses | ForEach-Object { $_.ProcessName } | Select-Object -Unique) -join ',' - If (-not($DisableLogging)) { + If (-not($DisableLogging)) { + If ($runningProcesses) { + [string]$runningProcessList = ($runningProcesses.ProcessName | Select-Object -Unique) -join ',' Write-Log -Message "The following processes are running: [$runningProcessList]." -Source ${CmdletName} - Write-Log -Message 'Resolve process descriptions...' -Source ${CmdletName} - } - ## Resolve the running process names to descriptions - ForEach ($runningProcess in $runningProcesses) { - ForEach ($processObject in $processObjects) { - If ($runningProcess.ProcessName -eq $processObject.ProcessName) { - If ($processObject.ProcessDescription) { - # The description of the process provided as a Parameter to the function, e.g. -ProcessName "winword=Microsoft Office Word". - $runningProcess | Add-Member -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $processObject.ProcessDescription -Force -PassThru -ErrorAction 'SilentlyContinue' - } - ElseIf ($runningProcess.Description) { - # If the process already has a description field specified, then use it - $runningProcess | Add-Member -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $runningProcess.Description -Force -PassThru -ErrorAction 'SilentlyContinue' - } - Else { - # Fall back on the process name if no description is provided by the process or as a parameter to the function - $runningProcess | Add-Member -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $runningProcess.ProcessName -Force -PassThru -ErrorAction 'SilentlyContinue' - } - } - } } - } - Else { - If (-not($DisableLogging)) { - Write-Log -Message 'Application(s) are not running.' -Source ${CmdletName} + Else { + Write-Log -Message 'Specified applications are not running.' -Source ${CmdletName} } } Write-Output -InputObject $runningProcesses + } Else { + Write-Output -InputObject $null } } End { @@ -6024,8 +6628,8 @@ Function Show-InstallationWelcome { # Get the deferral history from the registry $deferHistory = Get-DeferHistory - $deferHistoryTimes = $deferHistory | Select-Object -ExpandProperty 'DeferTimesRemaining' -ErrorAction 'SilentlyContinue' - $deferHistoryDeadline = $deferHistory | Select-Object -ExpandProperty 'DeferDeadline' -ErrorAction 'SilentlyContinue' + $deferHistoryTimes = $deferHistory.DeferTimesRemaining + $deferHistoryDeadline = $deferHistory.DeferDeadline # Reset Switches $checkDeferDays = $false @@ -6098,8 +6702,8 @@ Function Show-InstallationWelcome { } Set-Variable -Name 'closeAppsCountdownGlobal' -Value $closeAppsCountdown -Scope 'Script' - While ((Get-RunningProcesses -ProcessObjects $processObjects -OutVariable 'runningProcesses') -or (($promptResult -ne 'Defer') -and ($promptResult -ne 'Close'))) { - [string]$runningProcessDescriptions = ($runningProcesses | Where-Object { $_.ProcessDescription } | Select-Object -ExpandProperty 'ProcessDescription' | Select-Object -Unique | Sort-Object) -join ',' + While (($runningProcesses = Get-RunningProcesses -ProcessObjects $processObjects) -or (($promptResult -ne 'Defer') -and ($promptResult -ne 'Close'))) { + [string]$runningProcessDescriptions = ($runningProcesses.ProcessDescription | Sort-Object -Unique) -join ',' # Check if we need to prompt the user to defer, to defer and close apps, or not to prompt them at all If ($allowDefer) { # If there is deferral and closing apps is allowed but there are no apps to be closed, break the while loop @@ -6134,9 +6738,11 @@ Function Show-InstallationWelcome { If (($PromptToSave) -and ($SessionZero -and (-not $IsProcessUserInteractive))) { Write-Log -Message 'Specified [-PromptToSave] option will not be available because current process is running in session zero and is not interactive.' -Severity 2 -Source ${CmdletName} } - + # Update the process list right before closing, in case it changed + $runningProcesses = Get-RunningProcesses -ProcessObjects $processObjects + # Close running processes ForEach ($runningProcess in $runningProcesses) { - [psobject[]]$AllOpenWindowsForRunningProcess = Get-WindowTitle -GetAllWindowTitles -DisableFunctionLogging | Where-Object { $_.ParentProcess -eq $runningProcess.Name } + [psobject[]]$AllOpenWindowsForRunningProcess = Get-WindowTitle -GetAllWindowTitles -DisableFunctionLogging | Where-Object { $_.ParentProcess -eq $runningProcess.ProcessName } # If the PromptToSave parameter was specified and the process has a window open, then prompt the user to save work if there is work to be saved when closing window If (($PromptToSave) -and (-not ($SessionZero -and (-not $IsProcessUserInteractive))) -and ($AllOpenWindowsForRunningProcess) -and ($runningProcess.MainWindowHandle -ne [IntPtr]::Zero)) { [timespan]$PromptToSaveTimeout = New-TimeSpan -Seconds $configInstallationPromptToSave @@ -6144,11 +6750,11 @@ Function Show-InstallationWelcome { $PromptToSaveStopWatch.Reset() ForEach ($OpenWindow in $AllOpenWindowsForRunningProcess) { Try { - Write-Log -Message "Stop process [$($runningProcess.Name)] with window title [$($OpenWindow.WindowTitle)] and prompt to save if there is work to be saved (timeout in [$configInstallationPromptToSave] seconds)..." -Source ${CmdletName} + Write-Log -Message "Stop process [$($runningProcess.ProcessName)] with window title [$($OpenWindow.WindowTitle)] and prompt to save if there is work to be saved (timeout in [$configInstallationPromptToSave] seconds)..." -Source ${CmdletName} [boolean]$IsBringWindowToFrontSuccess = [PSADT.UiAutomation]::BringWindowToFront($OpenWindow.WindowHandle) [boolean]$IsCloseWindowCallSuccess = $runningProcess.CloseMainWindow() If (-not $IsCloseWindowCallSuccess) { - Write-Log -Message "Failed to call the CloseMainWindow() method on process [$($runningProcess.Name)] with window title [$($OpenWindow.WindowTitle)] because the main window may be disabled due to a modal dialog being shown." -Severity 3 -Source ${CmdletName} + Write-Log -Message "Failed to call the CloseMainWindow() method on process [$($runningProcess.ProcessName)] with window title [$($OpenWindow.WindowTitle)] because the main window may be disabled due to a modal dialog being shown." -Severity 3 -Source ${CmdletName} } Else { $PromptToSaveStopWatch.Start() @@ -6159,15 +6765,15 @@ Function Show-InstallationWelcome { } While (($IsWindowOpen) -and ($PromptToSaveStopWatch.Elapsed -lt $PromptToSaveTimeout)) $PromptToSaveStopWatch.Reset() If ($IsWindowOpen) { - Write-Log -Message "Exceeded the [$configInstallationPromptToSave] seconds timeout value for the user to save work associated with process [$($runningProcess.Name)] with window title [$($OpenWindow.WindowTitle)]." -Severity 2 -Source ${CmdletName} + Write-Log -Message "Exceeded the [$configInstallationPromptToSave] seconds timeout value for the user to save work associated with process [$($runningProcess.ProcessName)] with window title [$($OpenWindow.WindowTitle)]." -Severity 2 -Source ${CmdletName} } Else { - Write-Log -Message "Window [$($OpenWindow.WindowTitle)] for process [$($runningProcess.Name)] was successfully closed." -Source ${CmdletName} + Write-Log -Message "Window [$($OpenWindow.WindowTitle)] for process [$($runningProcess.ProcessName)] was successfully closed." -Source ${CmdletName} } } } Catch { - Write-Log -Message "Failed to close window [$($OpenWindow.WindowTitle)] for process [$($runningProcess.Name)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + Write-Log -Message "Failed to close window [$($OpenWindow.WindowTitle)] for process [$($runningProcess.ProcessName)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} Continue } Finally { @@ -6176,11 +6782,16 @@ Function Show-InstallationWelcome { } } Else { - Write-Log -Message "Stop process $($runningProcess.Name)..." -Source ${CmdletName} - Stop-Process -Id $runningProcess.Id -Force -ErrorAction 'SilentlyContinue' + Write-Log -Message "Stopping process $($runningProcess.ProcessName)..." -Source ${CmdletName} + Stop-Process -Name $runningProcess.ProcessName -Force -ErrorAction 'SilentlyContinue' } } - Start-Sleep -Seconds 2 + + if ($runningProcesses = Get-RunningProcesses -ProcessObjects $processObjects -DisableLogging) { + # Apps are still running, give them 2s to close. If they are still running, the Welcome Window will be displayed again + Write-Log -Message 'Sleeping for 2 seconds because the processes are still not closed...' -Source ${CmdletName} + Start-Sleep -Seconds 2 + } } # Stop the script (if not actioned before the timeout value) ElseIf ($promptResult -eq 'Timeout') { @@ -6224,25 +6835,25 @@ Function Show-InstallationWelcome { [array]$runningProcesses = $null [array]$runningProcesses = Get-RunningProcesses $processObjects If ($runningProcesses) { - [string]$runningProcessDescriptions = ($runningProcesses | Where-Object { $_.ProcessDescription } | Select-Object -ExpandProperty 'ProcessDescription' | Select-Object -Unique | Sort-Object) -join ',' + [string]$runningProcessDescriptions = ($runningProcesses.ProcessDescription | Sort-Object -Unique) -join ',' Write-Log -Message "Force close application(s) [$($runningProcessDescriptions)] without prompting user." -Source ${CmdletName} - $runningProcesses | Stop-Process -Force -ErrorAction 'SilentlyContinue' + $runningProcesses.ProcessName | ForEach-Object -Process {Stop-Process -Name $_ -Force -ErrorAction 'SilentlyContinue'} Start-Sleep -Seconds 2 } } ## Force nsd.exe to stop if Notes is one of the required applications to close - If (($processObjects | Select-Object -ExpandProperty 'ProcessName') -contains 'notes') { + If (($processObjects).ProcessName -contains 'notes') { ## Get the path where Notes is installed - [string]$notesPath = Get-Item -LiteralPath $regKeyLotusNotes -ErrorAction 'SilentlyContinue' | Get-ItemProperty | Select-Object -ExpandProperty 'Path' + [string]$notesPath = (Get-Item -LiteralPath $regKeyLotusNotes -ErrorAction 'SilentlyContinue' | Get-ItemProperty).Path ## Ensure we aren't running as a Local System Account and Notes install directory was found If ((-not $IsLocalSystemAccount) -and ($notesPath)) { # Get a list of all the executables in the Notes folder - [string[]]$notesPathExes = Get-ChildItem -LiteralPath $notesPath -Filter '*.exe' -Recurse | Select-Object -ExpandProperty 'BaseName' | Sort-Object + [string[]]$notesPathExes = (Get-ChildItem -LiteralPath $notesPath -Filter '*.exe' -Recurse).BaseName | Sort-Object ## Check for running Notes executables and run NSD if any are found $notesPathExes | ForEach-Object { - If ((Get-Process | Select-Object -ExpandProperty 'Name') -contains $_) { + If ((Get-Process).ProcessName -contains $_) { [string]$notesNSDExecutable = Join-Path -Path $notesPath -ChildPath 'NSD.exe' Try { If (Test-Path -LiteralPath $notesNSDExecutable -PathType 'Leaf' -ErrorAction 'Stop') { @@ -6269,7 +6880,7 @@ Function Show-InstallationWelcome { # Strip all Notes processes from the process list except notes.exe, because the other notes processes (e.g. notes2.exe) may be invoked by the Notes installation, so we don't want to block their execution. If ($notesPathExes) { - [array]$processesIgnoringNotesExceptions = Compare-Object -ReferenceObject ($processObjects | Select-Object -ExpandProperty 'ProcessName' | Sort-Object) -DifferenceObject $notesPathExes -IncludeEqual | Where-Object { ($_.SideIndicator -eq '<=') -or ($_.InputObject -eq 'notes') } | Select-Object -ExpandProperty 'InputObject' + [array]$processesIgnoringNotesExceptions = Compare-Object -ReferenceObject ($processObjects.ProcessName | Sort-Object) -DifferenceObject $notesPathExes -IncludeEqual | ForEach-Object { if (($_.SideIndicator -eq '<=') -or ($_.InputObject -eq 'notes')) {$_.InputObject} } [array]$processObjects = $processObjects | Where-Object { $processesIgnoringNotesExceptions -contains $_.ProcessName } } } @@ -6279,7 +6890,7 @@ Function Show-InstallationWelcome { # Make this variable globally available so we can check whether we need to call Unblock-AppExecution Set-Variable -Name 'BlockExecution' -Value $BlockExecution -Scope 'Script' Write-Log -Message '[-BlockExecution] parameter specified.' -Source ${CmdletName} - Block-AppExecution -ProcessName ($processObjects | Select-Object -ExpandProperty 'ProcessName') + Block-AppExecution -ProcessName ($processObjects.ProcessName) } } End { @@ -6377,10 +6988,8 @@ Function Show-WelcomePrompt { [datetime]$countdownTime = $startTime ## Check if the countdown was specified - If ($CloseAppsCountdown) { - If ($CloseAppsCountdown -gt $configInstallationUITimeout) { - Throw 'The close applications countdown time cannot be longer than the timeout specified in the XML configuration for installation UI dialogs to timeout.' - } + If ($CloseAppsCountdown -and ($CloseAppsCountdown -gt $configInstallationUITimeout)) { + Throw 'The close applications countdown time cannot be longer than the timeout specified in the XML configuration for installation UI dialogs to timeout.' } ## Initial form layout: Close Applications / Allow Deferral @@ -6405,8 +7014,7 @@ Function Show-WelcomePrompt { Write-Log -Message "Close applications countdown has [$closeAppsCountdown] seconds remaining." -Source ${CmdletName} $showCountdown = $true } - } - If ($showDefer) { + } Else { If ($persistPrompt) { $persistWindow = $true } } ## If 'force close apps countdown' was specified, enable that feature. @@ -6425,7 +7033,11 @@ Function Show-WelcomePrompt { $formWelcome = New-Object -TypeName 'System.Windows.Forms.Form' $pictureBanner = New-Object -TypeName 'System.Windows.Forms.PictureBox' + $labelWelcomeMessage = New-Object -TypeName 'System.Windows.Forms.Label' $labelAppName = New-Object -TypeName 'System.Windows.Forms.Label' + $labelCustomMessage = New-Object -TypeName 'System.Windows.Forms.Label' + $labelCloseAppsMessage = New-Object -TypeName 'System.Windows.Forms.Label' + $labelCountdownMessage = New-Object -TypeName 'System.Windows.Forms.Label' $labelCountdown = New-Object -TypeName 'System.Windows.Forms.Label' $labelDefer = New-Object -TypeName 'System.Windows.Forms.Label' $listBoxCloseApps = New-Object -TypeName 'System.Windows.Forms.ListBox' @@ -6441,8 +7053,12 @@ Function Show-WelcomePrompt { ## Remove all event handlers from the controls [scriptblock]$Form_Cleanup_FormClosed = { Try { + $labelWelcomeMessage.remove_Click($handler_labelWelcomeMessage_Click) $labelAppName.remove_Click($handler_labelAppName_Click) + $labelCustomMessage.remove_Click($handler_labelCustomMessage_Click) + $labelCloseAppsMessage.remove_Click($handler_labelCloseAppsMessage_Click) $labelDefer.remove_Click($handler_labelDefer_Click) + $labelCountdownMessage.remove_Click($handler_labelCountdownMessage_Click) $buttonCloseApps.remove_Click($buttonCloseApps_OnClick) $buttonContinue.remove_Click($buttonContinue_OnClick) $buttonDefer.remove_Click($buttonDefer_OnClick) @@ -6472,16 +7088,7 @@ Function Show-WelcomePrompt { ## Set up the form [timespan]$remainingTime = $countdownTime.Subtract($currentTime) - [string]$labelCountdownSeconds = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) - If ($forceCountdown -eq $true) { - switch ($deploymentType){ - 'Install' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeInstall.ToLower())) + "`n$labelCountdownSeconds" } - 'Uninstall' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeUninstall.ToLower())) + "`n$labelCountdownSeconds" } - 'Repair' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeRepair.ToLower())) + "`n$labelCountdownSeconds" } - Default { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeInstall.ToLower())) + "`n$labelCountdownSeconds" } - } - } - Else { $labelCountdown.Text = "$configClosePromptCountdownMessage`n$labelCountdownSeconds" } + $labelCountdown.Text = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) } ## Add the timer if it doesn't already exist - this avoids the timer being reset if the continue button is clicked @@ -6498,7 +7105,7 @@ Function Show-WelcomePrompt { Set-Variable -Name 'closeAppsCountdownGlobal' -Value $remainingTime.TotalSeconds -Scope 'Script' ## If the countdown is complete, close the application(s) or continue - If ($countdownTime -lt $currentTime) { + If ($countdownTime -le $currentTime) { If ($forceCountdown -eq $true) { Write-Log -Message 'Countdown timer has elapsed. Force continue.' -Source ${CmdletName} $buttonContinue.PerformClick() @@ -6511,16 +7118,7 @@ Function Show-WelcomePrompt { } Else { # Update the form - [string]$labelCountdownSeconds = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) - If ($forceCountdown -eq $true) { - switch ($deploymentType){ - 'Install' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeInstall) + "`n$labelCountdownSeconds" } - 'Uninstall' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeUninstall) + "`n$labelCountdownSeconds" } - 'Repair' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeRepair) + "`n$labelCountdownSeconds" } - Default { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeInstall) + "`n$labelCountdownSeconds" } - } - } - Else { $labelCountdown.Text = "$configClosePromptCountdownMessage`n$labelCountdownSeconds" } + $labelCountdown.Text = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) [Windows.Forms.Application]::DoEvents() } } @@ -6536,121 +7134,156 @@ Function Show-WelcomePrompt { If ($persistWindow) { $timerPersist = New-Object -TypeName 'System.Windows.Forms.Timer' $timerPersist.Interval = ($configInstallationPersistInterval * 1000) - [scriptblock]$timerPersist_Tick = { Update-InstallationWelcome } + [scriptblock]$timerPersist_Tick = { + $formWelcome.BringToFront() + $formWelcome.Location = "$($formWelcomeStartPosition.X),$($formWelcomeStartPosition.Y)" + $formWelcome.Refresh() + } $timerPersist.add_Tick($timerPersist_Tick) $timerPersist.Start() } ## Process Re-Enumeration Timer If ($configInstallationWelcomePromptDynamicRunningProcessEvaluation) { - $timerRunningProcesses = New-Object -TypeName 'System.Windows.Forms.Timer' - $timerRunningProcesses.Interval = ($configInstallationWelcomePromptDynamicRunningProcessEvaluationInterval * 1000) - [scriptblock]$timerRunningProcesses_Tick = { try { Get-RunningProcessesDynamically } catch {} } - $timerRunningProcesses.add_Tick($timerRunningProcesses_Tick) - $timerRunningProcesses.Start() + $timerRunningProcesses = New-Object -TypeName 'System.Windows.Forms.Timer' + $timerRunningProcesses.Interval = ($configInstallationWelcomePromptDynamicRunningProcessEvaluationInterval * 1000) + [scriptblock]$timerRunningProcesses_Tick = { try { + $dynamicRunningProcesses = $null + $dynamicRunningProcesses = Get-RunningProcesses -ProcessObjects $processObjects -DisableLogging + [string]$dynamicRunningProcessDescriptions = ($dynamicRunningProcesses.ProcessDescription | Sort-Object -Unique) -join ',' + If ($dynamicRunningProcessDescriptions -ne $script:runningProcessDescriptions) { + # Update the runningProcessDescriptions variable for the next time this function runs + Set-Variable -Name 'runningProcessDescriptions' -Value $dynamicRunningProcessDescriptions -Force -Scope 'Script' + If ($dynamicrunningProcesses) { + Write-Log -Message "The running processes have changed. Updating the apps to close: [$script:runningProcessDescriptions]..." -Source ${CmdletName} + } + # Update the list box with the processes to close + $listboxCloseApps.Items.Clear() + $script:runningProcessDescriptions -split "," | ForEach-Object { $null = $listboxCloseApps.Items.Add($_) } + } + # If CloseApps processes were running when the prompt was shown, and they are subsequently detected to be closed while the form is showing, then close the form. The deferral and CloseApps conditions will be re-evaluated. + If ($ProcessDescriptions) { + If (-not ($dynamicRunningProcesses)) { + Write-Log -Message 'Previously detected running processes are no longer running.' -Source ${CmdletName} + $formWelcome.Dispose() + } + } + # If CloseApps processes were not running when the prompt was shown, and they are subsequently detected to be running while the form is showing, then close the form for relaunch. The deferral and CloseApps conditions will be re-evaluated. + Else { + If ($dynamicRunningProcesses) { + Write-Log -Message 'New running processes detected. Updating the form to prompt to close the running applications.' -Source ${CmdletName} + $formWelcome.Dispose() + } + } + } catch {} } + $timerRunningProcesses.add_Tick($timerRunningProcesses_Tick) + $timerRunningProcesses.Start() } ## Form - $formWelcome.Controls.Add($pictureBanner) - $formWelcome.Controls.Add($buttonAbort) ##---------------------------------------------- - ## Create padding object - $paddingNone = New-Object -TypeName 'System.Windows.Forms.Padding' - $paddingNone.Top = 0 - $paddingNone.Bottom = 0 - $paddingNone.Left = 0 - $paddingNone.Right = 0 + ## Create zero px padding object + $paddingNone = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,0 + ## Create basic control size + $defaultControlSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,0 ## Generic Button properties - $buttonWidth = 110 - $buttonHeight = 23 - $buttonPadding = 50 - $buttonSize = New-Object -TypeName 'System.Drawing.Size' - $buttonSize.Width = $buttonWidth - $buttonSize.Height = $buttonHeight - $buttonPadding = New-Object -TypeName 'System.Windows.Forms.Padding' - $buttonPadding.Top = 0 - $buttonPadding.Bottom = 5 - $buttonPadding.Left = 50 - $buttonPadding.Right = 0 + $buttonSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 130,24 ## Picture Banner $pictureBanner.DataBindings.DefaultDataSourceUpdateMode = 0 $pictureBanner.ImageLocation = $appDeployLogoBanner - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 0 - $System_Drawing_Point.Y = 0 + $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,0 $pictureBanner.Location = $System_Drawing_Point $pictureBanner.Name = 'pictureBanner' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = $appDeployLogoBannerHeight - $System_Drawing_Size.Width = 450 + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,$appDeployLogoBannerHeight $pictureBanner.Size = $System_Drawing_Size $pictureBanner.SizeMode = 'CenterImage' $pictureBanner.Margin = $paddingNone - $pictureBanner.TabIndex = 0 $pictureBanner.TabStop = $false + ## Label Welcome Message + $labelWelcomeMessage.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelWelcomeMessage.Name = 'labelWelcomeMessage' + $labelWelcomeMessage.Size = $defaultControlSize + $labelWelcomeMessage.MinimumSize = $defaultControlSize + $labelWelcomeMessage.MaximumSize = $defaultControlSize + $labelWelcomeMessage.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,10,0,0 + $labelWelcomeMessage.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelWelcomeMessage.TabStop = $false + $labelWelcomeMessage.Text = $configDeferPromptWelcomeMessage + $labelWelcomeMessage.TextAlign = 'MiddleCenter' + $labelWelcomeMessage.Anchor = 'Top' + $labelWelcomeMessage.AutoSize = $true + $labelWelcomeMessage.add_Click($handler_labelWelcomeMessage_Click) + ## Label App Name $labelAppName.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelAppName.Font = 'Microsoft Sans Serif, 10pt, style=Bold' $labelAppName.Name = 'labelAppName' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - If (-not $showCloseApps) { - $System_Drawing_Size.Height = 40 - } - Else { - $System_Drawing_Size.Height = 65 - } - $System_Drawing_Size.Width = 450 - $labelAppName.Size = $System_Drawing_Size - $System_Drawing_Size.Height = 0 - $labelAppName.MaximumSize = $System_Drawing_Size - $labelAppName.Margin = '0,15,0,15' - $labelAppName.Padding = '20,0,20,0' - $labelAppName.TabIndex = 1 - - ## Initial form layout: Close Applications / Allow Deferral - If ($showCloseApps) { - $labelAppNameText = $configClosePromptMessage - } - ElseIf (($showDefer) -or ($forceCountdown)) { - $labelAppNameText = "$configDeferPromptWelcomeMessage `n$installTitle" - } - If ($CustomText) { - $labelAppNameText = "$labelAppNameText `n`n$configWelcomePromptCustomMessage" - } - $labelAppName.Text = $labelAppNameText - $labelAppName.TextAlign = 'TopCenter' + $labelAppName.Size = $defaultControlSize + $labelAppName.MinimumSize = $defaultControlSize + $labelAppName.MaximumSize = $defaultControlSize + $labelAppName.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,5,0,5 + $labelAppName.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelAppName.TabStop = $false + $labelAppName.Text = $installTitle + $labelAppName.TextAlign = 'MiddleCenter' $labelAppName.Anchor = 'Top' $labelAppName.AutoSize = $true $labelAppName.add_Click($handler_labelAppName_Click) + ## Label CustomMessage + $labelCustomMessage.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelCustomMessage.Name = 'labelCustomMessage' + $labelCustomMessage.Size = $defaultControlSize + $labelCustomMessage.MinimumSize = $defaultControlSize + $labelCustomMessage.MaximumSize = $defaultControlSize + $labelCustomMessage.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,5 + $labelCustomMessage.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelCustomMessage.TabStop = $false + $labelCustomMessage.Text = $configClosePromptMessage + $labelCustomMessage.TextAlign = 'MiddleCenter' + $labelCustomMessage.Anchor = 'Top' + $labelCustomMessage.AutoSize = $true + $labelCustomMessage.add_Click($handler_labelCustomMessage_Click) + + ## Label CloseAppsMessage + $labelCloseAppsMessage.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelCloseAppsMessage.Name = 'labelCloseAppsMessage' + $labelCloseAppsMessage.Size = $defaultControlSize + $labelCloseAppsMessage.MinimumSize = $defaultControlSize + $labelCloseAppsMessage.MaximumSize = $defaultControlSize + $labelCloseAppsMessage.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,5 + $labelCloseAppsMessage.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelCloseAppsMessage.TabStop = $false + $labelCloseAppsMessage.Text = $configClosePromptMessage + $labelCloseAppsMessage.TextAlign = 'MiddleCenter' + $labelCloseAppsMessage.Anchor = 'Top' + $labelCloseAppsMessage.AutoSize = $true + $labelCloseAppsMessage.add_Click($handler_labelCloseAppsMessage_Click) + ## Listbox Close Applications $listBoxCloseApps.DataBindings.DefaultDataSourceUpdateMode = 0 $listBoxCloseApps.FormattingEnabled = $true $listBoxCloseApps.HorizontalScrollbar = $true $listBoxCloseApps.Name = 'listBoxCloseApps' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 100 - $System_Drawing_Size.Width = 300 + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 420,100 $listBoxCloseApps.Size = $System_Drawing_Size - $listBoxCloseApps.Margin = '75,0,0,0' + $listBoxCloseApps.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 15,0,15,0 $listBoxCloseApps.TabIndex = 3 $ProcessDescriptions | ForEach-Object { $null = $listboxCloseApps.Items.Add($_) } ## Label Defer $labelDefer.DataBindings.DefaultDataSourceUpdateMode = 0 $labelDefer.Name = 'labelDefer' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 90 - $System_Drawing_Size.Width = 450 - $labelDefer.Size = $System_Drawing_Size - $System_Drawing_Size.Height = 0 - $labelDefer.MaximumSize = $System_Drawing_Size - $labelDefer.Margin = $paddingNone - $labelDefer.Padding = '40,0,20,0' - $labelDefer.TabIndex = 4 + $labelDefer.Size = $defaultControlSize + $labelDefer.MinimumSize = $defaultControlSize + $labelDefer.MaximumSize = $defaultControlSize + $labelDefer.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,5 + $labelDefer.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelDefer.TabStop = $false $deferralText = "$configDeferPromptExpiryMessage`n" If ($deferTimes -ge 0) { @@ -6668,80 +7301,126 @@ Function Show-WelcomePrompt { $labelDefer.AutoSize = $true $labelDefer.add_Click($handler_labelDefer_Click) + ## Label CountdownMessage + $labelCountdownMessage.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelCountdownMessage.Name = 'labelCountdownMessage' + $labelCountdownMessage.Size = $defaultControlSize + $labelCountdownMessage.MinimumSize = $defaultControlSize + $labelCountdownMessage.MaximumSize = $defaultControlSize + $labelCountdownMessage.Margin = $paddingNone + $labelCountdownMessage.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelCountdownMessage.TabStop = $false + If ($forceCountdown -eq $true) { + switch ($deploymentType){ + 'Uninstall' { $labelCountdownMessage.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeUninstall);break; } + 'Repair' { $labelCountdownMessage.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeRepair);break; } + Default { $labelCountdownMessage.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeInstall);break; } + } + } + Else { + $labelCountdownMessage.Text = $configClosePromptCountdownMessage + } + $labelCountdownMessage.TextAlign = 'MiddleCenter' + $labelCountdownMessage.Font = 'Microsoft Sans Serif, 9pt, style=Bold' + $labelCountdownMessage.Anchor = 'Top' + $labelCountdownMessage.AutoSize = $true + $labelCountdownMessage.add_Click($handler_labelCountdownMessage_Click) + ## Label Countdown $labelCountdown.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelCountdown.Font = 'Microsoft Sans Serif, 18pt, style=Bold' $labelCountdown.Name = 'labelCountdown' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 40 - $System_Drawing_Size.Width = 450 - $labelCountdown.Size = $System_Drawing_Size - $System_Drawing_Size.Height = 0 - $labelCountdown.MaximumSize = $System_Drawing_Size + $labelCountdown.Size = $defaultControlSize + $labelCountdown.MinimumSize = $defaultControlSize + $labelCountdown.MaximumSize = $defaultControlSize $labelCountdown.Margin = $paddingNone - $labelCountdown.Padding = '40,0,20,0' - $labelCountdown.TabIndex = 4 - $labelCountdown.Font = 'Microsoft Sans Serif, 9pt, style=Bold' + $labelCountdown.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelCountdown.TabStop = $false $labelCountdown.Text = '00:00:00' $labelCountdown.TextAlign = 'MiddleCenter' $labelCountdown.AutoSize = $true $labelCountdown.add_Click($handler_labelDefer_Click) ## Panel Flow Layout - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 0 - $System_Drawing_Point.Y = $appDeployLogoBannerHeight + $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,$appDeployLogoBannerHeight $flowLayoutPanel.Location = $System_Drawing_Point + $flowLayoutPanel.MinimumSize = $DefaultControlSize + $flowLayoutPanel.MaximumSize = $DefaultControlSize + $flowLayoutPanel.Size = $DefaultControlSize + $flowLayoutPanel.Margin = $paddingNone + $flowLayoutPanel.Padding = $paddingNone + $flowLayoutPanel.AutoSizeMode = "GrowAndShrink" $flowLayoutPanel.AutoSize = $true $flowLayoutPanel.Anchor = 'Top' $flowLayoutPanel.FlowDirection = 'TopDown' $flowLayoutPanel.WrapContents = $true + $flowLayoutPanel.Controls.Add($labelWelcomeMessage) $flowLayoutPanel.Controls.Add($labelAppName) - If ($showCloseApps) { $flowLayoutPanel.Controls.Add($listBoxCloseApps) } - If ($showDefer) { - $flowLayoutPanel.Controls.Add($labelDefer) + + If ($CustomText -and $configWelcomePromptCustomMessage) { + $labelCustomMessage.Text = $configWelcomePromptCustomMessage + $flowLayoutPanel.Controls.Add($labelCustomMessage) } - If ($showCountdown) { - $flowLayoutPanel.Controls.Add($labelCountdown) + If ($showCloseApps) { + $flowLayoutPanel.Controls.Add($labelCloseAppsMessage) + $flowLayoutPanel.Controls.Add($listBoxCloseApps) + } + If ($showDefer) { $flowLayoutPanel.Controls.Add($labelDefer) } + If ($showCountdown) { + $flowLayoutPanel.Controls.Add($labelCountdownMessage) + $flowLayoutPanel.Controls.Add($labelCountdown) } ## Button Close For Me $buttonCloseApps.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonCloseApps.Location = '15,0' + $buttonCloseApps.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 14,4 $buttonCloseApps.Name = 'buttonCloseApps' $buttonCloseApps.Size = $buttonSize - $buttonCloseApps.TabIndex = 5 + $buttonCloseApps.MinimumSize = $buttonSize + $buttonCloseApps.MaximumSize = $buttonSize + $buttonCloseApps.TabIndex = 1 $buttonCloseApps.Text = $configClosePromptButtonClose $buttonCloseApps.DialogResult = 'Yes' $buttonCloseApps.AutoSize = $true + $buttonCloseApps.Margin = $paddingNone + $buttonCloseApps.Padding = $paddingNone $buttonCloseApps.UseVisualStyleBackColor = $true $buttonCloseApps.add_Click($buttonCloseApps_OnClick) ## Button Defer $buttonDefer.DataBindings.DefaultDataSourceUpdateMode = 0 If (-not $showCloseApps) { - $buttonDefer.Location = '15,0' + $buttonDefer.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 14,4 } Else { - $buttonDefer.Location = '170,0' + $buttonDefer.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 160,4 } $buttonDefer.Name = 'buttonDefer' $buttonDefer.Size = $buttonSize - $buttonDefer.TabIndex = 6 + $buttonDefer.MinimumSize = $buttonSize + $buttonDefer.MaximumSize = $buttonSize + $buttonDefer.TabIndex = 0 $buttonDefer.Text = $configClosePromptButtonDefer $buttonDefer.DialogResult = 'No' $buttonDefer.AutoSize = $true + $buttonDefer.Margin = $paddingNone + $buttonDefer.Padding = $paddingNone $buttonDefer.UseVisualStyleBackColor = $true $buttonDefer.add_Click($buttonDefer_OnClick) ## Button Continue $buttonContinue.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonContinue.Location = '325,0' + $buttonContinue.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 306,4 $buttonContinue.Name = 'buttonContinue' $buttonContinue.Size = $buttonSize - $buttonContinue.TabIndex = 7 + $buttonContinue.MinimumSize = $buttonSize + $buttonContinue.MaximumSize = $buttonSize + $buttonContinue.TabIndex = 2 $buttonContinue.Text = $configClosePromptButtonContinue $buttonContinue.DialogResult = 'OK' $buttonContinue.AutoSize = $true + $buttonContinue.Margin = $paddingNone + $buttonContinue.Padding = $paddingNone $buttonContinue.UseVisualStyleBackColor = $true $buttonContinue.add_Click($buttonContinue_OnClick) If ($showCloseApps) { @@ -6756,18 +7435,26 @@ Function Show-WelcomePrompt { ## Button Abort (Hidden) $buttonAbort.DataBindings.DefaultDataSourceUpdateMode = 0 $buttonAbort.Name = 'buttonAbort' - $buttonAbort.Size = '1,1' + $buttonAbort.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 0,0 + $buttonAbort.MinimumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 0,0 + $buttonAbort.MaximumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 0,0 + $buttonAbort.BackColor = [System.Drawing.Color]::Transparent + $buttonAbort.ForeColor = [System.Drawing.Color]::Transparent + $buttonAbort.FlatAppearance.BorderSize = 0; + $buttonAbort.FlatAppearance.MouseDownBackColor = [System.Drawing.Color]::Transparent + $buttonAbort.FlatAppearance.MouseOverBackColor = [System.Drawing.Color]::Transparent + $buttonAbort.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat $buttonAbort.TabStop = $false $buttonAbort.DialogResult = 'Abort' - $buttonAbort.TabIndex = 5 + $buttonAbort.Visible = $true # Has to be set visible so we can call Click on it + $buttonAbort.Margin = $paddingNone + $buttonAbort.Padding = $paddingNone $buttonAbort.UseVisualStyleBackColor = $true $buttonAbort.add_Click($buttonAbort_OnClick) ## Form Welcome - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 0 - $System_Drawing_Size.Width = 0 - $formWelcome.Size = $System_Drawing_Size + $formWelcome.Size = $defaultControlSize + $formWelcome.MinimumSize = $defaultControlSize $formWelcome.Padding = $paddingNone $formWelcome.Margin = $paddingNone $formWelcome.DataBindings.DefaultDataSourceUpdateMode = 0 @@ -6782,32 +7469,22 @@ Function Show-WelcomePrompt { $formWelcome.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon $formWelcome.AutoSize = $true $formWelcome.Controls.Add($pictureBanner) - $formWelcome.Controls.Add($flowLayoutPanel) - + $formWelcome.Controls.Add($buttonAbort) ## Panel Button - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 0 - # Calculate the position of the panel relative to the size of the form - $System_Drawing_Point.Y = (($formWelcome.Size | Select-Object -ExpandProperty 'Height') - 10) - $panelButtons.Location = $System_Drawing_Point - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 40 - $System_Drawing_Size.Width = 450 - $panelButtons.Size = $System_Drawing_Size + $panelButtons.MinimumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,39 + $panelButtons.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,39 + $panelButtons.MaximumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,39 $panelButtons.AutoSize = $true - $panelButtons.Anchor = 'Top' - $padding = New-Object -TypeName 'System.Windows.Forms.Padding' - $padding.Top = 0 - $padding.Bottom = 0 - $padding.Left = 0 - $padding.Right = 0 - $panelButtons.Margin = $padding + $panelButtons.Padding = $paddingNone + $panelButtons.Margin = $paddingNone If ($showCloseApps) { $panelButtons.Controls.Add($buttonCloseApps) } If ($showDefer) { $panelButtons.Controls.Add($buttonDefer) } $panelButtons.Controls.Add($buttonContinue) - ## Add the Buttons Panel to the form - $formWelcome.Controls.Add($panelButtons) + ## Add the Buttons Panel to the flowPanel + $flowLayoutPanel.Controls.Add($panelButtons) + ## Add FlowPanel to the form + $formWelcome.Controls.Add($flowLayoutPanel) ## Save the initial state of the form $formWelcomeWindowState = $formWelcome.WindowState @@ -6816,43 +7493,6 @@ Function Show-WelcomePrompt { # Clean up the control events $formWelcome.add_FormClosed($Form_Cleanup_FormClosed) - Function Update-InstallationWelcome { - $formWelcome.BringToFront() - $formWelcome.Location = "$($formWelcomeStartPosition.X),$($formWelcomeStartPosition.Y)" - $formWelcome.Refresh() - } - - # Function invoked by a timer to periodically check running processes dynamically whilst showing the welcome prompt - Function Get-RunningProcessesDynamically { - $dynamicRunningProcesses = $null - Get-RunningProcesses -ProcessObjects $processObjects -DisableLogging -OutVariable 'dynamicRunningProcesses' - [string]$dynamicRunningProcessDescriptions = ($dynamicRunningProcesses | Where-Object { $_.ProcessDescription } | Select-Object -ExpandProperty 'ProcessDescription' | Select-Object -Unique | Sort-Object) -join ',' - If ($dynamicRunningProcessDescriptions -ne $script:runningProcessDescriptions) { - # Update the runningProcessDescriptions variable for the next time this function runs - Set-Variable -Name 'runningProcessDescriptions' -Value $dynamicRunningProcessDescriptions -Force -Scope 'Script' - If ($dynamicrunningProcesses) { - Write-Log -Message "The running processes have changed. Updating the apps to close: [$script:runningProcessDescriptions]..." -Source ${CmdletName} - } - # Update the list box with the processes to close - $listboxCloseApps.Items.Clear() - $script:runningProcessDescriptions -split "," | ForEach-Object { $null = $listboxCloseApps.Items.Add($_) } - } - # If CloseApps processes were running when the prompt was shown, and they are subsequently detected to be closed while the form is showing, then close the form. The deferral and CloseApps conditions will be re-evaluated. - If ($ProcessDescriptions) { - If (-not ($dynamicRunningProcesses)) { - Write-Log -Message 'Previously detected running processes are no longer running.' -Source ${CmdletName} - $formWelcome.Dispose() - } - } - # If CloseApps processes were not running when the prompt was shown, and they are subsequently detected to be running while the form is showing, then close the form for relaunch. The deferral and CloseApps conditions will be re-evaluated. - ElseIf (-not $ProcessDescriptions) { - If ($dynamicRunningProcesses) { - Write-Log -Message 'New running processes detected. Updating the form to prompt to close the running applications.' -Source ${CmdletName} - $formWelcome.Dispose() - } - } - } - ## Minimize all other windows If ($minimizeWindows) { $null = $shellApp.MinimizeAll() } @@ -6888,17 +7528,24 @@ Function Show-InstallationRestartPrompt { .DESCRIPTION Displays a restart prompt with a countdown to a forced restart. .PARAMETER CountdownSeconds - Specifies the number of seconds to countdown before the system restart. + Specifies the number of seconds to countdown before the system restart. Default: 60 .PARAMETER CountdownNoHideSeconds - Specifies the number of seconds to display the restart prompt without allowing the window to be hidden. + Specifies the number of seconds to display the restart prompt without allowing the window to be hidden. Default: 30 +.PARAMETER NoSilentRestart + Specifies whether the restart should be triggered when Deploy mode is silent or very silent. Default: $true .PARAMETER NoCountdown Specifies not to show a countdown, just the Restart Now and Restart Later buttons. The UI will restore/reposition itself persistently based on the interval value specified in the config file. +.PARAMETER SilentCountdownSeconds + Specifies number of seconds to countdown for the restart when the toolkit is running in silent mode and NoSilentRestart is $false. Default: 5 .EXAMPLE Show-InstallationRestartPrompt -Countdownseconds 600 -CountdownNoHideSeconds 60 .EXAMPLE Show-InstallationRestartPrompt -NoCountdown +.EXAMPLE + Show-InstallationRestartPrompt -Countdownseconds 300 -NoSilentRestart $false -SilentCountdownSeconds 10 .NOTES + Be mindful of the countdown you specify for the reboot as code directly after this function might NOT be able to execute - that includes logging. .LINK http://psappdeploytoolkit.com #> @@ -6911,7 +7558,12 @@ Function Show-InstallationRestartPrompt { [ValidateNotNullorEmpty()] [int32]$CountdownNoHideSeconds = 30, [Parameter(Mandatory=$false)] - [switch]$NoCountdown = $false + [bool]$NoSilentRestart = $true, + [Parameter(Mandatory=$false)] + [switch]$NoCountdown = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$SilentCountdownSeconds = 5 ) Begin { @@ -6920,9 +7572,15 @@ Function Show-InstallationRestartPrompt { Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header } Process { - ## Bypass if in non-interactive mode + ## If in non-interactive mode If ($deployModeSilent) { - Write-Log -Message "Bypass Installation Restart Prompt [Mode: $deployMode]." -Source ${CmdletName} + If ($NoSilentRestart -eq $false) { + Write-Log -Message "Triggering restart silently, because the deploy mode is set to [$deployMode] and [NoSilentRestart] is disabled. Timeout is set to [$SilentCountdownSeconds] seconds." -Source ${CmdletName} + Start-Process -FilePath "$PSHOME\powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -Command `"& {Start-Sleep -Seconds $SilentCountdownSeconds;Restart-Computer -Force;}`"" -WindowStyle 'Hidden' -ErrorAction 'SilentlyContinue' + } + Else { + Write-Log -Message "Skipping restart, because the deploy mode is set to [$deployMode] and [NoSilentRestart] is enabled." -Source ${CmdletName} + } Return } ## Get the parameters passed to the function for invoking the function asynchronously @@ -6943,9 +7601,11 @@ Function Show-InstallationRestartPrompt { $labelTimeRemaining = New-Object -TypeName 'System.Windows.Forms.Label' $labelMessage = New-Object -TypeName 'System.Windows.Forms.Label' $buttonRestartLater = New-Object -TypeName 'System.Windows.Forms.Button' - $picturebox = New-Object -TypeName 'System.Windows.Forms.PictureBox' + $pictureBanner = New-Object -TypeName 'System.Windows.Forms.PictureBox' $buttonRestartNow = New-Object -TypeName 'System.Windows.Forms.Button' $timerCountdown = New-Object -TypeName 'System.Windows.Forms.Timer' + $flowLayoutPanel = New-Object -TypeName 'System.Windows.Forms.FlowLayoutPanel' + $panelButtons = New-Object -TypeName 'System.Windows.Forms.Panel' $InitialFormWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' [scriptblock]$RestartComputer = { @@ -7016,7 +7676,7 @@ Function Show-InstallationRestartPrompt { [datetime]$countdownTime = $startTime.AddSeconds($countdownSeconds) [timespan]$remainingTime = $countdownTime.Subtract($currentTime) ## If the countdown is complete, restart the machine - If ($countdownTime -lt $currentTime) { + If ($countdownTime -le $currentTime) { $buttonRestartNow.PerformClick() } Else { @@ -7055,90 +7715,153 @@ Function Show-InstallationRestartPrompt { } ## Form - If (-not $NoCountdown) { - $formRestart.Controls.Add($labelCountdown) - $formRestart.Controls.Add($labelTimeRemaining) - } - $formRestart.Controls.Add($labelMessage) - $formRestart.Controls.Add($buttonRestartLater) - $formRestart.Controls.Add($picturebox) - $formRestart.Controls.Add($buttonRestartNow) - $clientSizeY = 260 + $appDeployLogoBannerHeightDifference - $formRestart.ClientSize = "450,$clientSizeY" - $formRestart.ControlBox = $false - $formRestart.FormBorderStyle = 'FixedDialog' - $formRestart.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon - $formRestart.MaximizeBox = $false - $formRestart.MinimizeBox = $false - $formRestart.Name = 'formRestart' - $formRestart.StartPosition = 'CenterScreen' - $formRestart.Text = "$($configRestartPromptTitle): $installTitle" - $formRestart.add_Load($FormEvent_Load) - $formRestart.add_Resize($formRestart_Resize) + ##---------------------------------------------- + ## Create zero px padding object + $paddingNone = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,0 + ## Create basic control size + $defaultControlSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,0 - ## Banner - $picturebox.Anchor = 'Top' - $picturebox.Image = [Drawing.Image]::Fromfile($AppDeployLogoBanner) - $picturebox.Location = '0,0' - $picturebox.Name = 'picturebox' - $pictureboxSizeY = $appDeployLogoBannerHeight - $picturebox.Size = "450,$pictureboxSizeY" - $picturebox.SizeMode = 'CenterImage' - $picturebox.TabIndex = 1 - $picturebox.TabStop = $false + ## Generic Button properties + $buttonSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 195,24 + + ## Picture Banner + $pictureBanner.DataBindings.DefaultDataSourceUpdateMode = 0 + $pictureBanner.ImageLocation = $appDeployLogoBanner + $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,0 + $pictureBanner.Location = $System_Drawing_Point + $pictureBanner.Name = 'pictureBanner' + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,$appDeployLogoBannerHeight + $pictureBanner.Size = $System_Drawing_Size + $pictureBanner.SizeMode = 'CenterImage' + $pictureBanner.Margin = $paddingNone + $pictureBanner.TabStop = $false ## Label Message - $labelMessageLocationY = 58 + $appDeployLogoBannerHeightDifference - $labelMessage.Location = "20,$labelMessageLocationY" + $labelMessage.DataBindings.DefaultDataSourceUpdateMode = 0 $labelMessage.Name = 'labelMessage' - $labelMessage.Size = '400,79' - $labelMessage.TabIndex = 3 + $labelMessage.Size = $defaultControlSize + $labelMessage.MinimumSize = $defaultControlSize + $labelMessage.MaximumSize = $defaultControlSize + $labelMessage.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,10,0,5 + $labelMessage.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 $labelMessage.Text = "$configRestartPromptMessage $configRestartPromptMessageTime `n`n$configRestartPromptMessageRestart" If ($NoCountdown) { $labelMessage.Text = $configRestartPromptMessage } $labelMessage.TextAlign = 'MiddleCenter' + $labelMessage.Anchor = 'Top' + $labelMessage.TabStop = $false + $labelMessage.AutoSize = $true - ## Label Time Remaining - $labelTimeRemainingLocationY = 138 + $appDeployLogoBannerHeightDifference - $labelTimeRemaining.Location = "20,$labelTimeRemainingLocationY" + ## Label Time remaining message + $labelTimeRemaining.DataBindings.DefaultDataSourceUpdateMode = 0 $labelTimeRemaining.Name = 'labelTimeRemaining' - $labelTimeRemaining.Size = '400,23' - $labelTimeRemaining.TabIndex = 4 + $labelTimeRemaining.Size = $defaultControlSize + $labelTimeRemaining.MinimumSize = $defaultControlSize + $labelTimeRemaining.MaximumSize = $defaultControlSize + $labelTimeRemaining.Margin = $paddingNone + $labelTimeRemaining.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelTimeRemaining.TabStop = $false + $labelTimeRemaining.Font = 'Microsoft Sans Serif, 9pt, style=Bold' $labelTimeRemaining.Text = $configRestartPromptTimeRemaining $labelTimeRemaining.TextAlign = 'MiddleCenter' + $labelTimeRemaining.Anchor = 'Top' + $labelTimeRemaining.AutoSize = $true ## Label Countdown + $labelCountdown.DataBindings.DefaultDataSourceUpdateMode = 0 $labelCountdown.Font = 'Microsoft Sans Serif, 18pt, style=Bold' - $labelCountdownLocationY = 165 + $appDeployLogoBannerHeightDifference - $labelCountdown.Location = "20,$labelCountdownLocationY" $labelCountdown.Name = 'labelCountdown' - $labelCountdown.Size = '400,30' - $labelCountdown.TabIndex = 5 + $labelCountdown.Size = $defaultControlSize + $labelCountdown.MinimumSize = $defaultControlSize + $labelCountdown.MaximumSize = $defaultControlSize + $labelCountdown.Margin = $paddingNone + $labelCountdown.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelCountdown.TabStop = $false $labelCountdown.Text = '00:00:00' $labelCountdown.TextAlign = 'MiddleCenter' + $labelCountdown.AutoSize = $true - # Generic Y location for buttons - $buttonsLocationY = 216 + $appDeployLogoBannerHeightDifference + ## Panel Flow Layout + $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,$appDeployLogoBannerHeight + $flowLayoutPanel.Location = $System_Drawing_Point + $flowLayoutPanel.MinimumSize = $DefaultControlSize + $flowLayoutPanel.MaximumSize = $DefaultControlSize + $flowLayoutPanel.Size = $DefaultControlSize + $flowLayoutPanel.Margin = $paddingNone + $flowLayoutPanel.Padding = $paddingNone + $flowLayoutPanel.AutoSizeMode = "GrowAndShrink" + $flowLayoutPanel.AutoSize = $true + $flowLayoutPanel.Anchor = 'Top' + $flowLayoutPanel.FlowDirection = 'TopDown' + $flowLayoutPanel.WrapContents = $true + $flowLayoutPanel.Controls.Add($labelMessage) + If (-not $NoCountdown) { + $flowLayoutPanel.Controls.Add($labelTimeRemaining) + $flowLayoutPanel.Controls.Add($labelCountdown) + } ## Label Restart Later - $buttonRestartLater.Anchor = 'Bottom,Left' - $buttonRestartLater.Location = "20,$buttonsLocationY" + $buttonRestartLater.DataBindings.DefaultDataSourceUpdateMode = 0 + $buttonRestartLater.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 240,4 $buttonRestartLater.Name = 'buttonRestartLater' - $buttonRestartLater.Size = '159,23' + $buttonRestartLater.Size = $buttonSize + $buttonRestartLater.MinimumSize = $buttonSize + $buttonRestartLater.MaximumSize = $buttonSize $buttonRestartLater.TabIndex = 0 $buttonRestartLater.Text = $configRestartPromptButtonRestartLater + $buttonRestartLater.AutoSize = $true + $buttonRestartLater.Margin = $paddingNone + $buttonRestartLater.Padding = $paddingNone $buttonRestartLater.UseVisualStyleBackColor = $true $buttonRestartLater.add_Click($buttonRestartLater_Click) ## Label Restart Now - $buttonRestartNow.Anchor = 'Bottom,Right' - $buttonRestartNow.Location = "265,$buttonsLocationY" + $buttonRestartNow.DataBindings.DefaultDataSourceUpdateMode = 0 + $buttonRestartNow.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 14,4 $buttonRestartNow.Name = 'buttonRestartNow' - $buttonRestartNow.Size = '159,23' - $buttonRestartNow.TabIndex = 2 + $buttonRestartNow.Size = $buttonSize + $buttonRestartNow.MinimumSize = $buttonSize + $buttonRestartNow.MaximumSize = $buttonSize + $buttonRestartNow.TabIndex = 1 $buttonRestartNow.Text = $configRestartPromptButtonRestartNow + $buttonRestartNow.Margin = $paddingNone + $buttonRestartNow.Padding = $paddingNone $buttonRestartNow.UseVisualStyleBackColor = $true $buttonRestartNow.add_Click($buttonRestartNow_Click) + ## Form Restart + $formRestart.Size = $defaultControlSize + $formRestart.MinimumSize = $defaultControlSize + $formRestart.Padding = $paddingNone + $formRestart.Margin = $paddingNone + $formRestart.DataBindings.DefaultDataSourceUpdateMode = 0 + $formRestart.Name = 'formRestart' + $formRestart.Text = $installTitle + $formRestart.StartPosition = 'CenterScreen' + $formRestart.FormBorderStyle = 'FixedDialog' + $formRestart.MaximizeBox = $false + $formRestart.MinimizeBox = $false + $formRestart.TopMost = $true + $formRestart.TopLevel = $true + $formRestart.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon + $formRestart.AutoSize = $true + $formRestart.ControlBox = $false + $formRestart.Controls.Add($pictureBanner) + + ## Button Panel + $panelButtons.MinimumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,39 + $panelButtons.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,39 + $panelButtons.MaximumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,39 + $panelButtons.AutoSize = $true + $panelButtons.Padding = $paddingNone + $panelButtons.Margin = $paddingNone + $panelButtons.Controls.Add($buttonRestartNow) + $panelButtons.Controls.Add($buttonRestartLater) + ## Add the Buttons Panel to the flowPanel + $flowLayoutPanel.Controls.Add($panelButtons) + ## Add FlowPanel to the form + $formRestart.Controls.Add($flowLayoutPanel) + $formRestart.add_Load($FormEvent_Load) + $formRestart.add_Resize($formRestart_Resize) ## Timer Countdown If (-not $NoCountdown) { $timerCountdown.add_Tick($timerCountdown_Tick) } @@ -7161,20 +7884,27 @@ Function Show-InstallationRestartPrompt { Else { Write-Log -Message "Invoking ${CmdletName} asynchronously with a [$countDownSeconds] second countdown..." -Source ${CmdletName} } + ## Remove Silent reboot parameters from the list that is being forwarded to the main script for asynchronous function execution. This is only for Interactive mode so we dont need silent mode reboot parameters. + $installRestartPromptParameters.Remove("NoSilentRestart") + $installRestartPromptParameters.Remove("SilentCountdownSeconds") + ## Prepare a list of parameters of this function as a string [string]$installRestartPromptParameters = ($installRestartPromptParameters.GetEnumerator() | ForEach-Object { - If ($_.Value.GetType().Name -eq 'SwitchParameter') { - "-$($_.Key)" - } - ElseIf ($_.Value.GetType().Name -eq 'Boolean') { - "-$($_.Key) `$" + "$($_.Value)".ToLower() - } - ElseIf ($_.Value.GetType().Name -eq 'Int32') { - "-$($_.Key) $($_.Value)" - } - Else { - "-$($_.Key) `"$($_.Value)`"" + switch ($_.Value.GetType().Name) { + 'SwitchParameter' { + "-$($_.Key)" + } + 'Boolean' { + "-$($_.Key) `$" + "$($_.Value)".ToLower() + } + 'Int32' { + "-$($_.Key) $($_.Value)" + } + default { + "-$($_.Key) `"$($_.Value)`"" + } } }) -join ' ' + ## Start another powershell instance silently with function parameters from this function Start-Process -FilePath "$PSHOME\powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$scriptPath`" -ReferredInstallTitle `"$installTitle`" -ReferredInstallName `"$installName`" -ReferredLogName `"$logName`" -ShowInstallationRestartPrompt $installRestartPromptParameters -AsyncToolkitLaunch" -WindowStyle 'Hidden' -ErrorAction 'SilentlyContinue' } Else { @@ -7215,7 +7945,9 @@ Function Show-BalloonTip { .PARAMETER BalloonTipIcon Icon to be used. Options: 'Error', 'Info', 'None', 'Warning'. Default is: Info. .PARAMETER BalloonTipTime - Time in milliseconds to display the balloon tip. Default: 500. + Time in milliseconds to display the balloon tip. Default: 10000. +.PARAMETER NoWait + Create the balloontip asynchronously. Default: $false .EXAMPLE Show-BalloonTip -BalloonTipText 'Installation Started' -BalloonTipTitle 'Application Name' .EXAMPLE @@ -7237,7 +7969,9 @@ Function Show-BalloonTip { [Windows.Forms.ToolTipIcon]$BalloonTipIcon = 'Info', [Parameter(Mandatory=$false,Position=3)] [ValidateNotNullorEmpty()] - [int32]$BalloonTipTime = 10000 + [int32]$BalloonTipTime = 10000, + [Parameter(Mandatory=$false,Position=4)] + [switch]$NoWait = $false ) Begin { @@ -7246,80 +7980,77 @@ Function Show-BalloonTip { Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header } Process { - ## Skip balloon if in silent mode - If (($deployModeSilent) -or (-not $configShowBalloonNotifications) -or (Test-PowerPoint)) { Return } - + ## Skip balloon if in silent mode, disabled in the config or presentation is detected + $presentationDetected = Test-PowerPoint + If (($deployModeSilent) -or (-not $configShowBalloonNotifications) -or $presentationDetected) { + Write-Log -Message "Bypassing Show-BalloonTip [Mode:$deployMode, Config Show Balloon Notifications:$configShowBalloonNotifications, Presentation Detected:$presentationDetected]. BalloonTipText:$BalloonTipText" -Source ${CmdletName} + Return + } ## Dispose of previous balloon If ($script:notifyIcon) { Try { $script:notifyIcon.Dispose() } Catch {} } - - ## Get the calling function so we know when to display the exiting balloon tip notification in an asynchronous script - Try { [string]$callingFunction = (Get-Variable -Name MyInvocation -Scope 1).Value.Mycommand.Name } Catch { } - - If ($callingFunction -eq 'Exit-Script') { + ## NoWait - Create the balloontip icon asynchronously + If ($NoWait) { Write-Log -Message "Display balloon tip notification asynchronously with message [$BalloonTipText]." -Source ${CmdletName} ## Create a script block to display the balloon notification in a new PowerShell process so that we can wait to cleanly dispose of the balloon tip without having to make the deployment script wait + ## Scriptblock text has to be as short as possible because it is passed as a parameter to powershell + ## Don't strongly type parameter BalloonTipIcon as System.Drawing assembly not loaded yet in asynchronous scriptblock so will throw error [scriptblock]$notifyIconScriptBlock = { - Param ( - [Parameter(Mandatory=$true,Position=0)] - [ValidateNotNullOrEmpty()] - [string]$BalloonTipText, - [Parameter(Mandatory=$false,Position=1)] - [ValidateNotNullorEmpty()] - [string]$BalloonTipTitle, - [Parameter(Mandatory=$false,Position=2)] - [ValidateSet('Error','Info','None','Warning')] - $BalloonTipIcon, # Don't strongly type variable as System.Drawing; assembly not loaded yet in asynchronous scriptblock so will throw error - [Parameter(Mandatory=$false,Position=3)] - [ValidateNotNullorEmpty()] - [int32]$BalloonTipTime, - [Parameter(Mandatory=$false,Position=4)] - [ValidateNotNullorEmpty()] - [string]$AppDeployLogoIcon - ) - - ## Load assembly containing class System.Windows.Forms and System.Drawing - Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' - Add-Type -AssemblyName 'System.Drawing' -ErrorAction 'Stop' - - [Windows.Forms.ToolTipIcon]$BalloonTipIcon = $BalloonTipIcon - $script:notifyIcon = New-Object -TypeName 'System.Windows.Forms.NotifyIcon' -Property @{ - BalloonTipIcon = $BalloonTipIcon - BalloonTipText = $BalloonTipText - BalloonTipTitle = $BalloonTipTitle - Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon - Text = -join $BalloonTipText[0..62] - Visible = $true - } - - ## Display the balloon tip notification asynchronously - $script:NotifyIcon.ShowBalloonTip($BalloonTipTime) - - ## Keep the asynchronous PowerShell process running so that we can dispose of the balloon tip icon - Start-Sleep -Milliseconds ($BalloonTipTime) - $script:notifyIcon.Dispose() - } - +Param( +[Parameter(Mandatory=$true,Position=0)] +[ValidateNotNullOrEmpty()] +[string]$BalloonTipText, +[Parameter(Mandatory=$false,Position=1)] +[ValidateNotNullorEmpty()] +[string]$BalloonTipTitle, +[Parameter(Mandatory=$false,Position=2)] +[ValidateSet('Error','Info','None','Warning')] +$BalloonTipIcon, +[Parameter(Mandatory=$false,Position=3)] +[ValidateNotNullorEmpty()] +[int32]$BalloonTipTime, +[Parameter(Mandatory=$false,Position=4)] +[ValidateNotNullorEmpty()] +[string]$AppDeployLogoIcon +) +Add-Type -AssemblyName 'System.Windows.Forms','System.Drawing' -ErrorAction 'Stop' +$BalloonTipIconText = [String]::Concat($BalloonTipTitle,' - ',$BalloonTipText) +if ($BalloonTipIconText.Length -gt 63) { $BalloonTipIconText = [String]::Concat($BalloonTipIconText.Substring(0,60),'...') } +[Windows.Forms.ToolTipIcon]$BalloonTipIcon = $BalloonTipIcon +$script:notifyIcon = New-Object -TypeName 'System.Windows.Forms.NotifyIcon' -Property @{ +BalloonTipIcon = $BalloonTipIcon +BalloonTipText = $BalloonTipText +BalloonTipTitle = $BalloonTipTitle +Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon +Text = $BalloonTipIconText +Visible = $true +} +$script:notifyIcon.ShowBalloonTip($BalloonTipTime) +Start-Sleep -Milliseconds ($BalloonTipTime) +$script:notifyIcon.Dispose() } ## Invoke a separate PowerShell process passing the script block as a command and associated parameters to display the balloon tip notification asynchronously Try { - Execute-Process -Path "$PSHOME\powershell.exe" -Parameters "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -Command & {$notifyIconScriptBlock} '$BalloonTipText' '$BalloonTipTitle' '$BalloonTipIcon' '$BalloonTipTime' '$AppDeployLogoIcon'" -NoWait -WindowStyle 'Hidden' -CreateNoWindow + Execute-Process -Path "$PSHOME\powershell.exe" -Parameters "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -Command & {$notifyIconScriptBlock} `'$BalloonTipText`' `'$BalloonTipTitle`' `'$BalloonTipIcon`' `'$BalloonTipTime`' `'$AppDeployLogoIcon`'" -NoWait -WindowStyle 'Hidden' -CreateNoWindow } Catch { } } ## Otherwise create the balloontip icon synchronously Else { Write-Log -Message "Display balloon tip notification with message [$BalloonTipText]." -Source ${CmdletName} + ## Prepare Text - Cut it if longer than 63 chars + $BalloonTipIconText = [String]::Concat($BalloonTipTitle,' - ',$BalloonTipText) + if ($BalloonTipIconText.Length -gt 63) { $BalloonTipIconText = [String]::Concat($BalloonTipIconText.Substring(0,60),'...') } + ## Create the BalloonTip [Windows.Forms.ToolTipIcon]$BalloonTipIcon = $BalloonTipIcon $script:notifyIcon = New-Object -TypeName 'System.Windows.Forms.NotifyIcon' -Property @{ BalloonTipIcon = $BalloonTipIcon BalloonTipText = $BalloonTipText BalloonTipTitle = $BalloonTipTitle Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon - Text = -join $BalloonTipText[0..62] + Text = $BalloonTipIconText Visible = $true } - ## Display the balloon tip notification - $script:NotifyIcon.ShowBalloonTip($BalloonTipTime) + $script:notifyIcon.ShowBalloonTip($BalloonTipTime) } } End { @@ -7363,7 +8094,7 @@ Function Show-InstallationProgress { [ValidateNotNullorEmpty()] [string]$StatusMessage = $configProgressMessageInstall, [Parameter(Mandatory=$false)] - [ValidateSet('Default','BottomRight')] + [ValidateSet('Default','BottomRight','TopCenter')] [string]$WindowLocation = 'Default', [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] @@ -7376,14 +8107,18 @@ Function Show-InstallationProgress { Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header } Process { - If ($deployModeSilent) { Return } + If ($deployModeSilent) { + Write-Log -Message "Bypassing Show-InstallationProgress [Mode: $deployMode]. Status message:$StatusMessage" -Source ${CmdletName} + Return + } ## If the default progress message hasn't been overridden and the deployment type is uninstall, use the default uninstallation message - If (($StatusMessage -eq $configProgressMessageInstall) -and ($deploymentType -eq 'Uninstall')) { - $StatusMessage = $configProgressMessageUninstall - } - If (($StatusMessage -eq $configProgressMessageInstall) -and ($deploymentType -eq 'Repair')) { - $StatusMessage = $configProgressMessageRepair + If ($StatusMessage -eq $configProgressMessageInstall) { + if ($deploymentType -eq 'Uninstall') { + $StatusMessage = $configProgressMessageUninstall + } elseif ($deploymentType -eq 'Repair') { + $StatusMessage = $configProgressMessageRepair + } } If ($envHost.Name -match 'PowerGUI') { @@ -7410,90 +8145,68 @@ Function Show-InstallationProgress { $script:ProgressRunspace.SessionStateProxy.SetVariable('windowLocation', $windowLocation) $script:ProgressRunspace.SessionStateProxy.SetVariable('topMost', $topMost.ToString()) $script:ProgressRunspace.SessionStateProxy.SetVariable('appDeployLogoBanner', $appDeployLogoBanner) - $script:ProgressRunspace.SessionStateProxy.SetVariable('appDeployLogoBannerHeight', $appDeployLogoBannerHeight) - $script:ProgressRunspace.SessionStateProxy.SetVariable('appDeployLogoBannerHeightDifference', $appDeployLogoBannerHeightDifference) $script:ProgressRunspace.SessionStateProxy.SetVariable('ProgressStatusMessage', $statusMessage) $script:ProgressRunspace.SessionStateProxy.SetVariable('AppDeployLogoIcon', $AppDeployLogoIcon) - $script:ProgressRunspace.SessionStateProxy.SetVariable('dpiScale', $dpiScale) # Add the script block to be executed in the progress runspace $progressCmd = [PowerShell]::Create().AddScript({ [string]$xamlProgressString = @' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + '@ - ## Replace dummy text with values and turn the string into an xml document variable - $xamlProgressString = $xamlProgressString.replace('%BannerHeight%', $appDeployLogoBannerHeight).replace('%Height%', 180 + $appDeployLogoBannerHeightDifference).replace('%MinHeight%', 180 + $appDeployLogoBannerHeightDifference).replace('%MaxHeight%', 200 + $appDeployLogoBannerHeightDifference) [Xml.XmlDocument]$xamlProgress = New-Object 'System.Xml.XmlDocument' $xamlProgress.LoadXml($xamlProgressString) ## Set the configurable values using variables added to the runspace from the parent thread - # Calculate the position on the screen where the progress dialog should be placed - $screen = [Windows.Forms.Screen]::PrimaryScreen - $screenWorkingArea = $screen.WorkingArea - [int32]$screenWidth = $screenWorkingArea | Select-Object -ExpandProperty 'Width' - [int32]$screenHeight = $screenWorkingArea | Select-Object -ExpandProperty 'Height' - # Set the start position of the Window based on the screen size - If ($windowLocation -eq 'BottomRight') { - $xamlProgress.Window.Left = [string](($screenWidth / ($dpiscale / 100)) - ($xamlProgress.Window.Width)) - $xamlProgress.Window.Top = [string](($screenHeight / ($dpiscale / 100)) - ($xamlProgress.Window.Height)) - } - # Show the default location (Top center) - Else { - # Center the progress window by calculating the center of the workable screen based on the width of the screen relative to the DPI scale minus half the width of the progress bar - $xamlProgress.Window.Left = [string](($screenWidth / (2 * ($dpiscale / 100) )) - (($xamlProgress.Window.Width / 2))) - $xamlProgress.Window.Top = [string]($screenHeight / 9.5) - } $xamlProgress.Window.TopMost = $topMost $xamlProgress.Window.Icon = $AppDeployLogoIcon $xamlProgress.Window.Grid.Image.Source = $appDeployLogoBanner @@ -7504,14 +8217,41 @@ Function Show-InstallationProgress { $script:ProgressSyncHash.Window = [Windows.Markup.XamlReader]::Load($progressReader) # Grey out the X button $script:ProgressSyncHash.Window.add_Loaded({ - [IntPtr]$windowHandle = (New-Object -TypeName System.Windows.Interop.WindowInteropHelper -ArgumentList $this).Handle - If ($null -ne $windowHandle) { - [IntPtr]$menuHandle = [PSADT.UiAutomation]::GetSystemMenu($windowHandle, $false) - If ($menuHandle -ne [IntPtr]::Zero) { - [PSADT.UiAutomation]::EnableMenuItem($menuHandle, 0xF060, 0x00000001) - [PSADT.UiAutomation]::DestroyMenu($menuHandle) + # Calculate the position on the screen where the progress dialog should be placed + [int32]$screenWidth = [System.Windows.SystemParameters]::WorkArea.Width + [int32]$screenHeight = [System.Windows.SystemParameters]::WorkArea.Height + [int32]$screenCenterWidth = $screenWidth - $script:ProgressSyncHash.Window.ActualWidth + [int32]$screenCenterHeight = $screenHeight - $script:ProgressSyncHash.Window.ActualHeight + # Set the start position of the Window based on the screen size + If ($windowLocation -eq 'BottomRight') { + # Put the window in the corner + $script:ProgressSyncHash.Window.Left = [Double]($screenCenterWidth) + $script:ProgressSyncHash.Window.Top = [Double]($screenCenterHeight) + } + ElseIf($windowLocation -eq 'TopCenter'){ + $script:ProgressSyncHash.Window.Left = [Double]($screenCenterWidth / 2) + $script:ProgressSyncHash.Window.Top = [Double]($screenCenterHeight / 6) + } + Else { + # Center the progress window by calculating the center of the workable screen based on the width of the screen minus half the width of the progress bar + $script:ProgressSyncHash.Window.Left = [Double]($screenCenterWidth / 2) + $script:ProgressSyncHash.Window.Top = [Double]($screenCenterHeight / 2) + } + # Grey out the X button + try { + $windowHandle = (New-Object -TypeName System.Windows.Interop.WindowInteropHelper -ArgumentList $this).Handle + If ($windowHandle -and ($windowHandle -ne [IntPtr]::Zero)) { + $menuHandle = [PSADT.UiAutomation]::GetSystemMenu($windowHandle, $false) + If ($menuHandle -and ($menuHandle -ne [IntPtr]::Zero)) { + [PSADT.UiAutomation]::EnableMenuItem($menuHandle, 0xF060, 0x00000001) + [PSADT.UiAutomation]::DestroyMenu($menuHandle) + } } } + catch { + # Not a terminating error if we can't grey out the button + Write-Log "Failed to grey out the Close button." -Severity 2 -Source ${CmdletName} + } }) # Prepare the ProgressText variable so we can use it to change the text in the text area $script:ProgressSyncHash.ProgressText = $script:ProgressSyncHash.Window.FindName('ProgressText') @@ -7528,7 +8268,7 @@ Function Show-InstallationProgress { $progressCmd.Runspace = $script:ProgressRunspace Write-Log -Message "Spin up progress dialog in a separate thread with message: [$statusMessage]." -Source ${CmdletName} # Invoke the progress runspace - $progressData = $progressCmd.BeginInvoke() + $null = $progressCmd.BeginInvoke() # Allow the thread to be spun up safely before invoking actions against it. Start-Sleep -Seconds 1 If ($script:ProgressSyncHash.Error) { @@ -7539,7 +8279,7 @@ Function Show-InstallationProgress { ElseIf ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -eq 'Running') { # Update the progress text Try { - $script:ProgressSyncHash.Window.Dispatcher.Invoke([Windows.Threading.DispatcherPriority]'Send', [Windows.Input.InputEventHandler]{ $script:ProgressSyncHash.ProgressText.Text = $statusMessage }, $null, $null) + $script:ProgressSyncHash.Window.Dispatcher.Invoke([Windows.Threading.DispatcherPriority]::Send, [Windows.Input.InputEventHandler]{ $script:ProgressSyncHash.ProgressText.Text = $statusMessage }, $null, $null) Write-Log -Message "Updated progress message: [$statusMessage]." -Source ${CmdletName} } Catch { @@ -7562,6 +8302,8 @@ Function Close-InstallationProgress { .DESCRIPTION Closes the dialog created by Show-InstallationProgress. This function is called by the Exit-Script function to close a running instance of the progress dialog if found. +.PARAMETER WaitingTime + How many seconds to wait, at most, for the InstallationProgress window to be initialized, before the function returns, without closing anything. Range: 1 - 60 Default: 5 .EXAMPLE Close-InstallationProgress .NOTES @@ -7571,6 +8313,9 @@ Function Close-InstallationProgress { #> [CmdletBinding()] Param ( + [Parameter(Mandatory=$false)] + [ValidateRange(1,60)] + [int]$WaitingTime = 5 ) Begin { @@ -7579,12 +8324,71 @@ Function Close-InstallationProgress { Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header } Process { - If ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -eq 'Running') { - ## Close the progress thread - Write-Log -Message 'Close the installation progress dialog.' -Source ${CmdletName} + If ($deployModeSilent) { + Write-Log -Message "Bypassing Close-InstallationProgress [Mode: $deployMode]" -Source ${CmdletName} + Return + } + # Check whether the window has been created and wait for up to $WaitingTime seconds if it does not + [int]$Timeout = $WaitingTime + while ((-not $script:ProgressSyncHash.Window.IsInitialized) -and ($Timeout -gt 0)) { + if ($Timeout -eq $WaitingTime) { + Write-Log -Message "The installation progress dialog does not exist. Waiting up to $WaitingTime seconds..." -Source ${CmdletName} + } + $Timeout -= 1 + Start-Sleep -Seconds 1 + } + # Return if we still have no window + if (-not $script:ProgressSyncHash.Window.IsInitialized) { + Write-Log -Message "The installation progress dialog was not created within $WaitingTime seconds." -Source ${CmdletName} -Severity 2 + return + } + # If the thread is suspended, resume it + if ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -band [system.threading.threadstate]::Suspended) { + Write-Log -Message 'The thread for the installation progress dialog is suspended. Resuming the thread.' -Source ${CmdletName} + try { + $script:ProgressSyncHash.Window.Dispatcher.Thread.Resume() + } + catch { + Write-Log -Message 'Failed to resume the thread for the installation progress dialog.' -Source ${CmdletName} -Severity 2 + } + } + # If the thread is changing its state, wait + [int]$Timeout = 0 + while ((($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -band [system.threading.threadstate]::Aborted) -or ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -band [system.threading.threadstate]::AbortRequested) -or ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -band [system.threading.threadstate]::StopRequested) -or ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -band [system.threading.threadstate]::Unstarted) -or ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -band [system.threading.threadstate]::WaitSleepJoin)) -and ($Timeout -le $WaitingTime)) { + if (-not $Timeout) { + Write-Log -Message "The thread for the installation progress dialog is changing its state. Waiting up to $WaitingTime seconds..." -Source ${CmdletName} -Severity 2 + } + $Timeout += 1 + Start-Sleep -Seconds 1 + } + # If the thread is running, stop it + if ((-not ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -band [system.threading.threadstate]::Stopped)) -and (-not ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -band [system.threading.threadstate]::Unstarted))) { + Write-Log -Message 'Closing the installation progress dialog.' -Source ${CmdletName} $script:ProgressSyncHash.Window.Dispatcher.InvokeShutdown() + } + + if ($script:ProgressRunspace) { + # If the runspace is still opening, wait + [int]$Timeout = 0 + while ((($script:ProgressRunspace.RunspaceStateInfo.State -eq [system.management.automation.runspaces.runspacestate]::Opening) -or ($script:ProgressRunspace.RunspaceStateInfo.State -eq [system.management.automation.runspaces.runspacestate]::BeforeOpen)) -and ($Timeout -le $WaitingTime)) { + if (-not $Timeout) { + Write-Log -Message "The runspace for the installation progress dialog is still opening. Waiting up to $WaitingTime seconds..." -Source ${CmdletName} -Severity 2 + } + $Timeout += 1 + Start-Sleep -Seconds 1 + } + # If the runspace is opened, close it + if ($script:ProgressRunspace.RunspaceStateInfo.State -eq [system.management.automation.runspaces.runspacestate]::Opened) { + Write-Log -Message "Closing the installation progress dialog`'s runspace." -Source ${CmdletName} + $script:ProgressRunspace.Close() + } + } else { + Write-Log -Message 'The runspace for the installation progress dialog is already closed.' -Source ${CmdletName} -Severity 2 + } + + if ($script:ProgressSyncHash) { + # Clear sync hash $script:ProgressSyncHash.Clear() - $script:ProgressRunspace.Close() } } End { @@ -7747,7 +8551,7 @@ Function Set-PinnedApplication { return } - $ExplorerCommandHandler = Get-RegistryKey -Key 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore\shell\Windows.taskbarpin' -Value 'ExplorerCommandHandler' + $ExplorerCommandHandler = Get-RegistryKey -Key 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore\shell\Windows.taskbarpin' -Value 'ExplorerCommandHandler' $classesStarKey = (Get-Item "Registry::HKEY_USERS\$($RunasActiveUser.SID)\SOFTWARE\Classes").OpenSubKey("*", $true) $shellKey = $classesStarKey.CreateSubKey("shell", $true) $specialKey = $shellKey.CreateSubKey("{:}", $true) @@ -8107,7 +8911,7 @@ Function Invoke-RegisterOrUnregisterDLL { } } - [psobject]$ExecuteResult = Execute-Process -Path $RegSvr32Path -Parameters $DLLActionParameters -WindowStyle 'Hidden' -PassThru + [psobject]$ExecuteResult = Execute-Process -Path $RegSvr32Path -Parameters $DLLActionParameters -WindowStyle 'Hidden' -PassThru -ExitOnProcessFailure $false If ($ExecuteResult.ExitCode -ne 0) { If ($ExecuteResult.ExitCode -eq 60002) { @@ -8388,7 +9192,7 @@ Function Get-MsiTableProperty { $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Close' -ArgumentList @() Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($View) } Catch { } } - ElseIf($SummaryInformation) { + ElseIf ($SummaryInformation) { Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($SummaryInformation) } Catch { } } } @@ -8829,7 +9633,7 @@ Function Install-MSUpdates { # Installation type for executables (i.e., Microsoft Office Updates) '.exe' { Execute-Process -Path $file.FullName -Parameters '/quiet /norestart' -WindowStyle 'Hidden' -IgnoreExitCodes "*" } # Installation type for Windows updates using Windows Update Standalone Installer - '.msu' { Execute-Process -Path 'wusa.exe' -Parameters "`"$($file.FullName)`" /quiet /norestart" -WindowStyle 'Hidden' -IgnoreExitCodes "*" } + '.msu' { Execute-Process -Path $exeWusa -Parameters "`"$($file.FullName)`" /quiet /norestart" -WindowStyle 'Hidden' -IgnoreExitCodes "*" } # Installation type for Windows Installer Patch '.msp' { Execute-MSI -Action 'Patch' -Path $file.FullName -IgnoreExitCodes "*" } } @@ -8918,7 +9722,7 @@ Function Get-WindowTitle { [psobject]$VisibleWindow = New-Object -TypeName 'PSObject' -Property @{ WindowTitle = $VisibleWindowTitle WindowHandle = $VisibleWindowHandle - ParentProcess= $Process.Name + ParentProcess= $Process.ProcessName ParentProcessMainWindowHandle = $Process.MainWindowHandle ParentProcessId = $Process.Id } @@ -9008,9 +9812,6 @@ Function Send-Keys { [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - ## Load assembly containing class System.Windows.Forms.SendKeys - Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' - [scriptblock]$SendKeys = { Param ( [Parameter(Mandatory=$true)] @@ -9107,9 +9908,6 @@ Function Test-Battery { [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - ## PowerStatus class found in this assembly is more reliable than WMI in cases where the battery is failing. - Add-Type -Assembly 'System.Windows.Forms' -ErrorAction 'SilentlyContinue' - ## Initialize a hashtable to store information about system type and power status [hashtable]$SystemTypePowerStatus = @{ } } @@ -9138,7 +9936,7 @@ Function Test-Battery { } $SystemTypePowerStatus.Add('BatteryLifePercent', $PowerStatus.BatteryLifePercent) - ## The reported approximate number of seconds of battery life remaining. It will report –1 if the remaining life is unknown because the system is on AC power. + ## The reported approximate number of seconds of battery life remaining. It will report -1 if the remaining life is unknown because the system is on AC power. [int32]$BatteryLifeRemaining = $PowerStatus.BatteryLifeRemaining $SystemTypePowerStatus.Add('BatteryLifeRemaining', $PowerStatus.BatteryLifeRemaining) @@ -9150,20 +9948,22 @@ Function Test-Battery { ## Determine if the system is using AC power [boolean]$OnACPower = $false - If ($PowerLineStatus -eq 'Online') { - Write-Log -Message 'System is using AC power.' -Source ${CmdletName} - $OnACPower = $true - } - ElseIf ($PowerLineStatus -eq 'Offline') { - Write-Log -Message 'System is using battery power.' -Source ${CmdletName} - } - ElseIf ($PowerLineStatus -eq 'Unknown') { - If (($BatteryChargeStatus -eq 'NoSystemBattery') -or ($BatteryChargeStatus -eq 'Unknown')) { - Write-Log -Message "System power status is [$PowerLineStatus] and battery charge status is [$BatteryChargeStatus]. This is most likely due to a damaged battery so we will report system is using AC power." -Source ${CmdletName} - $OnACPower = $true + switch ($PowerLineStatus) { + 'Online' { + Write-Log -Message 'System is using AC power.' -Source ${CmdletName} + $OnACPower = $true } - Else { - Write-Log -Message "System power status is [$PowerLineStatus] and battery charge status is [$BatteryChargeStatus]. Therefore, we will report system is using battery power." -Source ${CmdletName} + 'Offline' { + Write-Log -Message 'System is using battery power.' -Source ${CmdletName} + } + 'Unknown' { + If (($BatteryChargeStatus -eq 'NoSystemBattery') -or ($BatteryChargeStatus -eq 'Unknown')) { + Write-Log -Message "System power status is [$PowerLineStatus] and battery charge status is [$BatteryChargeStatus]. This is most likely due to a damaged battery so we will report system is using AC power." -Source ${CmdletName} + $OnACPower = $true + } + Else { + Write-Log -Message "System power status is [$PowerLineStatus] and battery charge status is [$BatteryChargeStatus]. Therefore, we will report system is using battery power." -Source ${CmdletName} + } } } $SystemTypePowerStatus.Add('IsUsingACPower', $OnACPower) @@ -9177,12 +9977,14 @@ Function Test-Battery { $IsLaptop = $true } # Chassis Types - [int32[]]$ChassisTypes = Get-WmiObject -Class 'Win32_SystemEnclosure' | Where-Object { $_.ChassisTypes } | Select-Object -ExpandProperty 'ChassisTypes' + [int32[]]$ChassisTypes = (Get-WmiObject -Class 'Win32_SystemEnclosure').ChassisTypes Write-Log -Message "The following system chassis types were detected [$($ChassisTypes -join ',')]." -Source ${CmdletName} ForEach ($ChassisType in $ChassisTypes) { Switch ($ChassisType) { - { $_ -eq 9 -or $_ -eq 10 -or $_ -eq 14 } { $IsLaptop = $true } # 9=Laptop, 10=Notebook, 14=Sub Notebook - { $_ -eq 3 } { $IsLaptop = $false } # 3=Desktop + 9 { $IsLaptop = $true; } # 9=Laptop + 10 { $IsLaptop = $true; } # 10=Notebook + 14 { $IsLaptop = $true; } # 14=Sub Notebook + 3 { $IsLaptop = $false; } # 3=Desktop } } # Add IsLaptop property to hashtable @@ -9294,7 +10096,7 @@ Function Test-PowerPoint { If ([Environment]::UserInteractive) { # Check if "POWERPNT" process has a window with a title that begins with "PowerPoint Slide Show" or "Powerpoint-" for non-English language systems. # There is a possiblity of a false positive if the PowerPoint filename starts with "PowerPoint Slide Show" - [psobject]$PowerPointWindow = Get-WindowTitle -GetAllWindowTitles | Where-Object { $_.WindowTitle -match '^PowerPoint Slide Show' -or $_.WindowTitle -match '^PowerPoint-' } | Where-Object { $_.ParentProcess -eq 'POWERPNT'} | Select-Object -First 1 + [psobject]$PowerPointWindow = Get-WindowTitle -GetAllWindowTitles | Where-Object { ($_.WindowTitle -match '^PowerPoint Slide Show' -or $_.WindowTitle -match '^PowerPoint-') -and ($_.ParentProcess -eq 'POWERPNT') } | Select-Object -First 1 If ($PowerPointWindow) { [nullable[boolean]]$IsPowerPointFullScreen = $true Write-Log -Message 'Detected that PowerPoint process [POWERPNT] has a window with a title that beings with [PowerPoint Slide Show] or [PowerPoint-].' -Source ${CmdletName} @@ -9403,8 +10205,13 @@ Function Invoke-SCCMTask { ## Determine the SCCM Client Version Try { - [version]$SCCMClientVersion = Get-WmiObject -Namespace 'ROOT\CCM' -Class 'CCM_InstalledComponent' -ErrorAction 'Stop' | Where-Object { $_.Name -eq 'SmsClient' } | Select-Object -ExpandProperty 'Version' -ErrorAction 'Stop' - Write-Log -Message "Installed SCCM Client Version Number [$SCCMClientVersion]." -Source ${CmdletName} + [version]$SCCMClientVersion = Get-WmiObject -Namespace 'ROOT\CCM' -Class 'CCM_InstalledComponent' -ErrorAction 'Stop' | ForEach-Object { if($_.Name -eq 'SmsClient') {$_.Version} } + if ($SCCMClientVersion) { + Write-Log -Message "Installed SCCM Client Version Number [$SCCMClientVersion]." -Source ${CmdletName} + } else { + Write-Log -Message "Failed to determine the SCCM client version number. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + Throw 'Failed to determine the SCCM client version number.' + } } Catch { Write-Log -Message "Failed to determine the SCCM client version number. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} @@ -9526,8 +10333,13 @@ Function Install-SCCMSoftwareUpdates { ## Determine the SCCM Client Version Try { - [version]$SCCMClientVersion = Get-WmiObject -Namespace 'ROOT\CCM' -Class 'CCM_InstalledComponent' -ErrorAction 'Stop' | Where-Object { $_.Name -eq 'SmsClient' } | Select-Object -ExpandProperty 'Version' -ErrorAction 'Stop' - Write-Log -Message "Installed SCCM Client Version Number [$SCCMClientVersion]." -Source ${CmdletName} + [version]$SCCMClientVersion = Get-WmiObject -Namespace 'ROOT\CCM' -Class 'CCM_InstalledComponent' -ErrorAction 'Stop' | ForEach-Object { if($_.Name -eq 'SmsClient') {$_.Version} } + if ($SCCMClientVersion) { + Write-Log -Message "Installed SCCM Client Version Number [$SCCMClientVersion]." -Source ${CmdletName} + } else { + Write-Log -Message "Failed to determine the SCCM client version number. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + Throw 'Failed to determine the SCCM client version number.' + } } Catch { Write-Log -Message "Failed to determine the SCCM client version number. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} @@ -9626,7 +10438,7 @@ Function Update-GroupPolicy { [string]$InstallMsg = 'Update Group Policies for the User' Write-Log -Message "$($InstallMsg)..." -Source ${CmdletName} } - [psobject]$ExecuteResult = Execute-Process -Path "$envWindir\system32\cmd.exe" -Parameters $GPUpdateCmd -WindowStyle 'Hidden' -PassThru + [psobject]$ExecuteResult = Execute-Process -Path "$envWinDir\system32\cmd.exe" -Parameters $GPUpdateCmd -WindowStyle 'Hidden' -PassThru -ExitOnProcessFailure $false If ($ExecuteResult.ExitCode -ne 0) { If ($ExecuteResult.ExitCode -eq 60002) { @@ -9684,7 +10496,7 @@ Function Enable-TerminalServerInstallMode { Process { Try { Write-Log -Message 'Change terminal server into user install mode...' -Source ${CmdletName} - $terminalServerResult = & change.exe User /Install + $terminalServerResult = & "$envWinDir\System32\change.exe" User /Install If ($global:LastExitCode -ne 1) { Throw $terminalServerResult } } @@ -9732,7 +10544,7 @@ Function Disable-TerminalServerInstallMode { Process { Try { Write-Log -Message 'Change terminal server into user execute mode...' -Source ${CmdletName} - $terminalServerResult = & change.exe User /Execute + $terminalServerResult = & "$envWinDir\System32\change.exe" User /Execute If ($global:LastExitCode -ne 1) { Throw $terminalServerResult } } @@ -9782,6 +10594,8 @@ Function Set-ActiveSetup { Remove Active Setup entry from HKLM registry hive. Will also load each logon user's HKCU registry hive to remove Active Setup entry. .PARAMETER DisableActiveSetup Disables the Active Setup entry so that the StubPath file will not be executed. +.PARAMETER ExecuteForCurrentUser + Specifies whether the StubExePath should be executed for the current user. Since this user is already logged in, the user won't have the application started without logging out and logging back in. Default: $True .PARAMETER ContinueOnError Continue if an error is encountered. Default is: $true. .EXAMPLE @@ -9821,6 +10635,9 @@ Function Set-ActiveSetup { [switch]$DisableActiveSetup = $false, [Parameter(Mandatory=$true,ParameterSetName='Purge')] [switch]$PurgeActiveSetupKey, + [Parameter(Mandatory=$false,ParameterSetName='Create')] + [ValidateNotNullorEmpty()] + [boolean]$ExecuteForCurrentUser = $true, [Parameter(Mandatory=$false)] [ValidateNotNullorEmpty()] [boolean]$ContinueOnError = $true @@ -9833,15 +10650,15 @@ Function Set-ActiveSetup { } Process { Try { - [string]$ActiveSetupKey = "HKLM:SOFTWARE\Microsoft\Active Setup\Installed Components\$Key" - [string]$HKCUActiveSetupKey = "HKCU:Software\Microsoft\Active Setup\Installed Components\$Key" + [string]$ActiveSetupKey = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Active Setup\Installed Components\$Key" + [string]$HKCUActiveSetupKey = "Registry::HKEY_CURRENT_USER\Software\Microsoft\Active Setup\Installed Components\$Key" ## Delete Active Setup registry entry from the HKLM hive and for all logon user registry hives on the system If ($PurgeActiveSetupKey) { - Write-Log -Message "Remove Active Setup entry [$ActiveSetupKey]." -Source ${CmdletName} + Write-Log -Message "Removing Active Setup entry [$ActiveSetupKey]." -Source ${CmdletName} Remove-RegistryKey -Key $ActiveSetupKey -Recurse - Write-Log -Message "Remove Active Setup entry [$HKCUActiveSetupKey] for all log on user registry hives on the system." -Source ${CmdletName} + Write-Log -Message "Removing Active Setup entry [$HKCUActiveSetupKey] for all log on user registry hives on the system." -Source ${CmdletName} [scriptblock]$RemoveHKCUActiveSetupKey = { If (Get-RegistryKey -Key $HKCUActiveSetupKey -SID $UserProfile.SID) { Remove-RegistryKey -Key $HKCUActiveSetupKey -SID $UserProfile.SID -Recurse @@ -9877,7 +10694,12 @@ Function Set-ActiveSetup { [string]$CUArguments = $Arguments [string]$StubPath = "$CUStubExePath" } - {'.vbs','.js' -contains $StubExeExt} { + '.js' { + [string]$CUStubExePath = "$envWinDir\system32\cscript.exe" + [string]$CUArguments = "//nologo `"$StubExePath`"" + [string]$StubPath = "$CUStubExePath $CUArguments" + } + '.vbs' { [string]$CUStubExePath = "$envWinDir\system32\cscript.exe" [string]$CUArguments = "//nologo `"$StubExePath`"" [string]$StubPath = "$CUStubExePath $CUArguments" @@ -9934,33 +10756,51 @@ Function Set-ActiveSetup { } } + + Write-Log -Message "Adding Active Setup Key for local machine: [$ActiveSetupKey]." -Source ${CmdletName} & $SetActiveSetupRegKeys -ActiveSetupRegKey $ActiveSetupKey ## Execute the StubPath file for the current user as long as not in Session 0 - If ($SessionZero) { - If ($RunAsActiveUser) { - Write-Log -Message "Session 0 detected: Execute Active Setup StubPath file for currently logged in user [$($RunAsActiveUser.NTAccount)]." -Source ${CmdletName} - If ($CUArguments) { - Execute-ProcessAsUser -Path $CUStubExePath -Parameters $CUArguments -Wait -ContinueOnError $true + If ($ExecuteForCurrentUser) { + If ($SessionZero) { + If ($RunAsActiveUser) { + # Skip if Active Setup reg key is present and IsInstalled is 1 + If ((Get-RegistryKey -Key $HKCUActiveSetupKey -SID $UserProfile.SID -Value "IsInstalled" -ContinueOnError $true) -ne 1) { + Write-Log -Message "Session 0 detected: Executing Active Setup StubPath file for currently logged in user [$($RunAsActiveUser.NTAccount)]." -Source ${CmdletName} + If ($CUArguments) { + Execute-ProcessAsUser -Path $CUStubExePath -Parameters $CUArguments -Wait -ContinueOnError $true + } + Else { + Execute-ProcessAsUser -Path $CUStubExePath -Wait -ContinueOnError $true + } + + Write-Log -Message "Adding Active Setup Key for the current user: [$HKCUActiveSetupKey]." -Source ${CmdletName} + & $SetActiveSetupRegKeys -ActiveSetupRegKey $HKCUActiveSetupKey -SID $RunAsActiveUser.SID + } else { + Write-Log -Message "Session 0 detected: Skipping executing Active Setup StubPath file for currently logged in user [$($RunAsActiveUser.NTAccount)], because Active Setup registry key already exists and has IsInstalled set to 1." -Source ${CmdletName} -Severity 2 + } } Else { - Execute-ProcessAsUser -Path $CUStubExePath -Wait -ContinueOnError $true + Write-Log -Message 'Session 0 detected: No logged in users detected. Active Setup StubPath file will execute when users first log into their account.' -Source ${CmdletName} } - & $SetActiveSetupRegKeys -ActiveSetupRegKey $HKCUActiveSetupKey -SID $RunAsActiveUser.SID } Else { - Write-Log -Message 'Session 0 detected: No logged in users detected. Active Setup StubPath file will execute when users first log into their account.' -Source ${CmdletName} - } - } - Else { - Write-Log -Message 'Execute Active Setup StubPath file for the current user.' -Source ${CmdletName} - If ($CUArguments) { - $ExecuteResults = Execute-Process -FilePath $CUStubExePath -Parameters $CUArguments -PassThru - } - Else { - $ExecuteResults = Execute-Process -FilePath $CUStubExePath -PassThru + # Skip if Active Setup reg key is present and IsInstalled is 1 + If ((Get-RegistryKey -Key $HKCUActiveSetupKey -Value "IsInstalled" -ContinueOnError $true) -ne 1) { + Write-Log -Message 'Executing Active Setup StubPath file for the current user.' -Source ${CmdletName} + If ($CUArguments) { + $ExecuteResults = Execute-Process -FilePath $CUStubExePath -Parameters $CUArguments -PassThru -ExitOnProcessFailure $false + } + Else { + $ExecuteResults = Execute-Process -FilePath $CUStubExePath -PassThru -ExitOnProcessFailure $false + } + + Write-Log -Message "Adding Active Setup Key for the current user: [$HKCUActiveSetupKey]." -Source ${CmdletName} + & $SetActiveSetupRegKeys -ActiveSetupRegKey $HKCUActiveSetupKey + } else { + Write-Log -Message "Skipping executing Active Setup StubPath file for current user, because Active Setup registry key already exists and has IsInstalled set to 1." -Source ${CmdletName} -Severity 2 + } } - & $SetActiveSetupRegKeys -ActiveSetupRegKey $HKCUActiveSetupKey } } Catch { @@ -9990,7 +10830,7 @@ Function Test-ServiceExists { .PARAMETER ComputerName Specify the name of the computer. Default is: the local computer. .PARAMETER PassThru - Return the WMI service object. + Return the WMI service object. To see all the properties use: Test-ServiceExists -Name 'spooler' -PassThru | Get-Member .PARAMETER ContinueOnError Continue if an error is encountered. Default is: $true. .EXAMPLE @@ -10145,7 +10985,7 @@ Function Stop-ServiceAndDependencies { Stop-Service -InputObject (Get-Service -ComputerName $ComputerName -Name $DependentService.ServiceName -ErrorAction 'Stop') -Force -WarningAction 'SilentlyContinue' -ErrorAction 'Stop' } Catch { - Write-Log -Message "Failed to start dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]. Continue..." -Severity 2 -Source ${CmdletName} + Write-Log -Message "Failed to stop dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]. Continue..." -Severity 2 -Source ${CmdletName} Continue } } @@ -10349,8 +11189,8 @@ Function Get-ServiceStartMode ## If on Windows Vista or higher, check to see if service is set to Automatic (Delayed Start) If (($ServiceStartMode -eq 'Automatic') -and (([version]$envOSVersion).Major -gt 5)) { Try { - [string]$ServiceRegistryPath = "HKLM:SYSTEM\CurrentControlSet\Services\$Name" - [int32]$DelayedAutoStart = Get-ItemProperty -LiteralPath $ServiceRegistryPath -ErrorAction 'Stop' | Select-Object -ExpandProperty 'DelayedAutoStart' -ErrorAction 'Stop' + [string]$ServiceRegistryPath = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\$Name" + [int32]$DelayedAutoStart = (Get-ItemProperty -LiteralPath $ServiceRegistryPath -ErrorAction 'Stop').DelayedAutoStart If ($DelayedAutoStart -eq 1) { $ServiceStartMode = 'Automatic (Delayed Start)' } } Catch { } @@ -10423,12 +11263,14 @@ Function Set-ServiceStartMode ## Set the name of the start up mode that will be passed to sc.exe [string]$ScExeStartMode = $StartMode - If ($StartMode -eq 'Automatic') { $ScExeStartMode = 'Auto' } - If ($StartMode -eq 'Automatic (Delayed Start)') { $ScExeStartMode = 'Delayed-Auto' } - If ($StartMode -eq 'Manual') { $ScExeStartMode = 'Demand' } + switch ($StartMode) { + 'Automatic' { $ScExeStartMode = 'Auto'; break } + 'Automatic (Delayed Start)' { $ScExeStartMode = 'Delayed-Auto'; break } + 'Manual' { $ScExeStartMode = 'Demand'; break } + } ## Set the start up mode using sc.exe. Note: we found that the ChangeStartMode method in the Win32_Service WMI class set services to 'Automatic (Delayed Start)' even when you specified 'Automatic' on Win7, Win8, and Win10. - $ChangeStartMode = & sc.exe config $Name start= $ScExeStartMode + $ChangeStartMode = & "$envWinDir\System32\sc.exe" config $Name start= $ScExeStartMode If ($global:LastExitCode -ne 0) { Throw "sc.exe failed with exit code [$($global:LastExitCode)] and message [$ChangeStartMode]." @@ -10478,10 +11320,13 @@ Function Get-LoggedOnUser { Description of IsActiveUserSession property: If a console user exists, then that will be the active user session. - If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user that is either 'Active' or 'Connected' is the active user. + If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user that has ConnectState either 'Active' or 'Connected' is the active user. Description of IsRdpSession property: Gets a value indicating whether the user is associated with an RDP client session. + + Description of IsLocalAdmin property: + Checks whether the user is a member of the Administrators group .LINK http://psappdeploytoolkit.com #> @@ -10546,7 +11391,7 @@ Function Get-PendingReboot { Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header ## Initialize variables - [string]$private:ComputerName = ([Net.Dns]::GetHostEntry('')).HostName + [string]$private:ComputerName = $envComputerNameFQDN $PendRebootErrorMsg = $null } Process { @@ -10565,7 +11410,7 @@ Function Get-PendingReboot { ## Determine if a Windows Vista/Server 2008 and above machine has a pending reboot from a Component Based Servicing (CBS) operation Try { If (([version]$envOSVersion).Major -ge 5) { - If (Test-Path -LiteralPath 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' -ErrorAction 'Stop') { + If (Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' -ErrorAction 'Stop') { [nullable[boolean]]$IsCBServicingRebootPending = $true } Else { @@ -10581,7 +11426,7 @@ Function Get-PendingReboot { ## Determine if there is a pending reboot from a Windows Update Try { - If (Test-Path -LiteralPath 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' -ErrorAction 'Stop') { + If (Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' -ErrorAction 'Stop') { [nullable[boolean]]$IsWindowsUpdateRebootPending = $true } Else { @@ -10597,12 +11442,12 @@ Function Get-PendingReboot { ## Determine if there is a pending reboot from a pending file rename operation [boolean]$IsFileRenameRebootPending = $false $PendingFileRenameOperations = $null - If (Test-RegistryValue -Key 'HKLM:SYSTEM\CurrentControlSet\Control\Session Manager' -Value 'PendingFileRenameOperations') { + If (Test-RegistryValue -Key 'Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager' -Value 'PendingFileRenameOperations') { # If PendingFileRenameOperations value exists, set $IsFileRenameRebootPending variable to $true [boolean]$IsFileRenameRebootPending = $true # Get the value of PendingFileRenameOperations Try { - [string[]]$PendingFileRenameOperations = Get-ItemProperty -LiteralPath 'HKLM:SYSTEM\CurrentControlSet\Control\Session Manager' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'PendingFileRenameOperations' -ErrorAction 'Stop' + [string[]]$PendingFileRenameOperations = Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'PendingFileRenameOperations' -ErrorAction 'Stop' } Catch { [string[]]$PendRebootErrorMsg += "Failed to get PendingFileRenameOperations: $($_.Exception.Message)" @@ -10643,8 +11488,7 @@ Function Get-PendingReboot { ## Determine if there is a pending reboot from an App-V global Pending Task. (User profile based tasks will complete on logoff/logon) Try { - If (Test-Path -LiteralPath 'HKLM:SOFTWARE\Software\Microsoft\AppV\Client\PendingTasks' -ErrorAction 'Stop') { - + If (Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Software\Microsoft\AppV\Client\PendingTasks' -ErrorAction 'Stop') { [nullable[boolean]]$IsAppVRebootPending = $true } Else { @@ -10686,6 +11530,221 @@ Function Get-PendingReboot { } #endregion +#region Function Set-ItemPermission +Function Set-ItemPermission { + <# + .SYNOPSIS + Allow you to easily change permissions on files or folders + .PARAMETER Path + Path to the folder or file you want to modify (ex: C:\Temp) + .PARAMETER User + One or more user names (ex: BUILTIN\Users, DOMAIN\Admin) to give the permissions to. If you want to use SID, prefix it with an asterisk * (ex: *S-1-5-18) + .PARAMETER Permission + Permission or list of permissions to be set/added/removed/replaced. To see all the possible permissions go to 'http://technet.microsoft.com/fr-fr/library/ff730951.aspx'. + Permission DeleteSubdirectoriesAndFiles does not apply to files. + .PARAMETER PermissionType + Sets Access Control Type of the permissions. Allowed options: Allow, Deny Default: Allow + .PARAMETER Inheritance + Sets permission inheritance. Does not apply to files. Multiple options can be specified. Allowed options: ObjectInherit, ContainerInherit, None Default: None + None - The permission entry is not inherited by child objects, ObjectInherit - The permission entry is inherited by child leaf objects. ContainerInherit - The permission entry is inherited by child container objects. + .PARAMETER Propagation + Sets how to propagate inheritance. Does not apply to files. Allowed options: None, InheritOnly, NoPropagateInherit Default: None + None - Specifies that no inheritance flags are set. NoPropagateInherit - Specifies that the permission entry is not propagated to child objects. InheritOnly - Specifies that the permission entry is propagated only to child objects. This includes both container and leaf child objects. + .PARAMETER Method + Specifies which method will be used to apply the permissions. Allowed options: Add, Set, Reset. + Add - adds permissions rules but it does not remove previous permissions, Set - overwrites matching permission rules with new ones, Reset - removes matching permissions rules and then adds permission rules, Remove - Removes matching permission rules, RemoveSpecific - Removes specific permissions, RemoveAll - Removes all permission rules for specified user/s + Default: Add + .PARAMETER EnableInheritance + Enables inheritance on the files/folders. + .EXAMPLE + Will grant FullControl permissions to 'John' and 'Users' on 'C:\Temp' and its files and folders children. + PS C:\>Set-ItemPermission -Path "C:\Temp" -User "DOMAIN\John", "BUILTIN\Utilisateurs" -Permission FullControl -Inheritance ObjectInherit,ContainerInherit + .EXAMPLE + Will grant Read permissions to 'John' on 'C:\Temp\pic.png' + PS C:\>Set-ItemPermission -Path "C:\Temp\pic.png" -User "DOMAIN\John" -Permission Read + .EXAMPLE + Will remove all permissions to 'John' on 'C:\Temp\Private' + PS C:\>Set-ItemPermission -Path "C:\Temp\Private" -User "DOMAIN\John" -Permission None -Method RemoveAll + .NOTES + Original Author : Julian DA CUNHA - dacunha.julian@gmail.com, used with permission + .LINK + http://psappdeploytoolkit.com + #> + + [CmdletBinding()] + Param ( + [Parameter( Mandatory=$True, Position=0, HelpMessage = "Path to the folder or file you want to modify (ex: C:\Temp)",ParameterSetName="DisableInheritance" )] + [Parameter( Mandatory=$True, Position=0, HelpMessage = "Path to the folder or file you want to modify (ex: C:\Temp)",ParameterSetName="EnableInheritance" )] + [ValidateNotNullOrEmpty()] + [Alias('File', 'Folder')] + [String]$Path, + + [Parameter( Mandatory=$True, Position=1, HelpMessage = "One or more user names (ex: BUILTIN\Users, DOMAIN\Admin). If you want to use SID, prefix it with an asterisk * (ex: *S-1-5-18)", ParameterSetName="DisableInheritance")] + [Alias('Username', 'Users', 'SID', 'Usernames')] + [String[]]$User, + + [Parameter( Mandatory=$True, Position=2, HelpMessage = "Permission or list of permissions to be set/added/removed/replaced. To see all the possible permissions go to 'http://technet.microsoft.com/fr-fr/library/ff730951.aspx'", ParameterSetName="DisableInheritance")] + [Alias('Acl', 'Grant', 'Permissions', 'Deny')] + [ValidateSet("AppendData", "ChangePermissions", "CreateDirectories", "CreateFiles", "Delete", ` + "DeleteSubdirectoriesAndFiles", "ExecuteFile", "FullControl", "ListDirectory", "Modify",` + "Read", "ReadAndExecute", "ReadAttributes", "ReadData", "ReadExtendedAttributes", "ReadPermissions",` + "Synchronize", "TakeOwnership", "Traverse", "Write", "WriteAttributes", "WriteData", "WriteExtendedAttributes", "None")] + [String[]]$Permission, + + [Parameter( Mandatory=$False, Position=3, HelpMessage = "Whether you want to set Allow or Deny permissions", ParameterSetName="DisableInheritance")] + [Alias('AccessControlType')] + [ValidateSet("Allow", "Deny")] + [String]$PermissionType = "Allow", + + [Parameter( Mandatory=$False, Position=4, HelpMessage = "Sets how permissions are inherited", ParameterSetName="DisableInheritance")] + [ValidateSet("ContainerInherit", "None", "ObjectInherit")] + [String[]]$Inheritance = "None", + + [Parameter( Mandatory=$False, Position=5, HelpMessage = "Sets how to propage inheritance flags", ParameterSetName="DisableInheritance")] + [ValidateSet("None", "InheritOnly", "NoPropagateInherit")] + [String]$Propagation = "None", + + [Parameter( Mandatory=$False, Position=6, HelpMessage = "Specifies which method will be used to add/remove/replace permissions.", ParameterSetName="DisableInheritance")] + [ValidateSet("Add", "Set", "Reset", "Remove", "RemoveSpecific", "RemoveAll")] + [Alias("ApplyMethod", "ApplicationMethod")] + [String]$Method = "Add", + + [Parameter( Mandatory=$True, Position=1, HelpMessage = "Enables inheritance, which removes explicit permissions.", ParameterSetName="EnableInheritance")] + [switch]$EnableInheritance + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + + Process { + # Test elevated perms + If (-not $IsAdmin){ + Write-Log -Message "Unable to use the function [Set-ItemPermission] without elevated permissions." -Source ${CmdletName} + Throw "Unable to use the function [Set-ItemPermission] without elevated permissions." + } + + # Check path existence + If (-not (Test-Path -Path $Path -ErrorAction Stop)) { + Write-Log -Message "Specified path does not exist [$Path]." -Source ${CmdletName} + Throw "Specified path does not exist [$Path]." + } + + If ($EnableInheritance) { + # Get object acls + $Acl = (get-item -Path $Path -ErrorAction Stop).GetAccessControl('Access') + # Enable inherance + $Acl.SetAccessRuleProtection($False, $True) + Write-Log -Message "Enabling Inheritance on path [$Path]." -Source ${CmdletName} + $null = Set-Acl -Path $Path -AclObject $Acl -ErrorAction Stop + return + } + # Permissions + [System.Security.AccessControl.FileSystemRights]$FileSystemRights = New-Object System.Security.AccessControl.FileSystemRights + If ($Permission -ne "None") { + foreach ($Entry in $Permission) { + $FileSystemRights = $FileSystemRights -bor [System.Security.AccessControl.FileSystemRights]$Entry + } + } + + # InheritanceFlags + $InheritanceFlag = New-Object System.Security.AccessControl.InheritanceFlags + foreach ($IFlag in $Inheritance) { + $InheritanceFlag = $InheritanceFlag -bor [System.Security.AccessControl.InheritanceFlags]$IFlag + } + + # PropagationFlags + $PropagationFlag = [System.Security.AccessControl.PropagationFlags]$Propagation + + # Access Control Type + $Allow = [System.Security.AccessControl.AccessControlType]$PermissionType + + # Modify variables to remove file incompatible flags if this is a file + If (Test-Path -Path $Path -ErrorAction Stop -PathType Leaf) { + $FileSystemRights = $FileSystemRights -band (-bnot [System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles) + $InheritanceFlag = [System.Security.AccessControl.InheritanceFlags]::None + $PropagationFlag = [System.Security.AccessControl.PropagationFlags]::None + } + + # Get object acls + $Acl = (get-item -Path $Path -ErrorAction Stop).GetAccessControl('Access') + # Disable inherance, Preserve inherited permissions + $Acl.SetAccessRuleProtection($True, $True) + $null = Set-Acl -Path $Path -AclObject $Acl -ErrorAction Stop + # Get updated acls - without inheritance + $Acl = $null + $Acl = (get-item -Path $Path -ErrorAction Stop).GetAccessControl('Access') + # Apply permissions on Users + Foreach ($U in $User) { + # Trim whitespace and skip if empty + $U = $U.Trim() + If ($U.Length -eq 0) { + continue + } + # Set Username + If ($U.StartsWith('*')) { + # This is a SID, remove the * + $U = $U.remove(0,1) + try { + # Translate the SID + $UsersAccountName = ConvertTo-NTAccountOrSID -SID $U + } + catch { + Write-Log "Failed to translate SID [$U]. Skipping..." -Source ${CmdletName} -Severity 2 + continue + } + + $Username = New-Object System.Security.Principal.NTAccount($UsersAccountName) + } else { + $Username = New-Object System.Security.Principal.NTAccount($U) + } + + # Set/Add/Remove/Replace permissions and log the changes + $Rule = New-Object System.Security.AccessControl.FileSystemAccessRule($Username, $FileSystemRights, $InheritanceFlag, $PropagationFlag, $Allow) + switch ($Method) { + "Add" { + Write-Log -Message "Setting permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.AddAccessRule($Rule) + break + } + "Set" { + Write-Log -Message "Setting permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.SetAccessRule($Rule) + break + } + "Reset" { + Write-Log -Message "Setting permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.ResetAccessRule($Rule) + break + } + "Remove" { + Write-Log -Message "Removing permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.RemoveAccessRule($Rule) + break + } + "RemoveSpecific" { + Write-Log -Message "Removing permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.RemoveAccessRuleSpecific($Rule) + break + } + "RemoveAll" { + Write-Log -Message "Removing permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.RemoveAccessRuleAll($Rule) + break + } + } + } + # Use the prepared ACL + $null = Set-Acl -Path $Path -AclObject $Acl -ErrorAction Stop + } + + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } +} +#endregion #endregion ##*============================================= @@ -10731,30 +11790,6 @@ If (-not ([Management.Automation.PSTypeName]'PSADT.UiAutomation').Type) { } } -## Define ScriptBlock to test for and attempt to make a service healthy by checking if it exists, is currently running, and has the specified start mode. -[scriptblock]$TestServiceHealth = { - Param ( - [string]$ServiceName, - [string]$ServiceStartMode = 'Automatic' - ) - Try { - [boolean]$IsServiceHealthy = $true - If (Test-ServiceExists -Name $ServiceName -ContinueOnError $false) { - If ((Get-ServiceStartMode -Name $ServiceName -ContinueOnError $false) -ne $ServiceStartMode) { - Set-ServiceStartMode -Name $ServiceName -StartMode $ServiceStartMode -ContinueOnError $false - } - Start-ServiceAndDependencies -Name $ServiceName -SkipServiceExistsTest -ContinueOnError $false - } - Else { - [boolean]$IsServiceHealthy = $false - } - } - Catch { - [boolean]$IsServiceHealthy = $false - } - Write-Output -InputObject $IsServiceHealthy -} - ## Disable logging until log file details are available . $DisableScriptLogging @@ -10762,9 +11797,9 @@ If (-not ([Management.Automation.PSTypeName]'PSADT.UiAutomation').Type) { If ((-not $appName) -and (-not $ReferredInstallName)){ # Build properly formatted Architecture String switch ($Is64Bit) { - $false { $formattedOSArch = "x86" } - $true { $formattedOSArch = "x64" } - } + $false { $formattedOSArch = "x86" } + $true { $formattedOSArch = "x64" } + } # Find the first MSI file in the Files folder and use that as our install if ([string]$defaultMsiFile = (Get-ChildItem -LiteralPath $dirFiles -ErrorAction 'SilentlyContinue' | Where-Object { (-not $_.PsIsContainer) -and ([IO.Path]::GetExtension($_.Name) -eq ".msi") -and ($_.Name.EndsWith(".$formattedOSArch.msi")) } | Select-Object -ExpandProperty 'FullName' -First 1)) { Write-Log -Message "Discovered $formattedOSArch Zerotouch MSI under $defaultMSIFile" -Source $appDeployToolkitName @@ -10785,7 +11820,7 @@ If ((-not $appName) -and (-not $ReferredInstallName)){ [string]$defaultMstFile = '' } # Discover if there are zero-config MSP files. Name multiple MSP files in alphabetical order to control order in which they are installed. - [string[]]$defaultMspFiles = Get-ChildItem -LiteralPath $dirFiles -ErrorAction 'SilentlyContinue' | Where-Object { (-not $_.PsIsContainer) -and ([IO.Path]::GetExtension($_.Name) -eq '.msp') } | Select-Object -ExpandProperty 'FullName' + [string[]]$defaultMspFiles = Get-ChildItem -LiteralPath $dirFiles -ErrorAction 'SilentlyContinue' | ForEach-Object { if((-not $_.PsIsContainer) -and ([IO.Path]::GetExtension($_.Name) -eq '.msp')) {$_.FullName} } If ($defaultMspFiles) { Write-Log -Message "Discovered Zero-Config MSP installation file(s) [$($defaultMspFiles -join ',')]." -Source $appDeployToolkitName } @@ -10799,7 +11834,7 @@ If ((-not $appName) -and (-not $ReferredInstallName)){ [string]$appVersion = $defaultMsiPropertyList.ProductVersion $GetDefaultMsiTablePropertySplat.Set_Item('Table', 'File') [psobject]$defaultMsiFileList = Get-MsiTableProperty @GetDefaultMsiTablePropertySplat - [string[]]$defaultMsiExecutables = Get-Member -InputObject $defaultMsiFileList -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Name' -ErrorAction 'Stop' | Where-Object { [IO.Path]::GetExtension($_) -eq '.exe' } | ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($_) } + [string[]]$defaultMsiExecutables = Get-Member -InputObject $defaultMsiFileList -ErrorAction 'Stop' | ForEach-Object { if([IO.Path]::GetExtension($_.Name) -eq '.exe') {[IO.Path]::GetFileNameWithoutExtension($_.Name)} } [string]$defaultMsiExecutablesList = $defaultMsiExecutables -join ',' Write-Log -Message "App Vendor [$appVendor]." -Source $appDeployToolkitName Write-Log -Message "App Name [$appName]." -Source $appDeployToolkitName @@ -10821,6 +11856,12 @@ If (-not $appName) { If (-not $appLang) { [string]$appLang = $currentLanguage } If (-not $appRevision) { [string]$appRevision = '01' } If (-not $appArch) { [string]$appArch = '' } +} else { + If (-not $appVendor) { [string]$appVendor = '' } + If (-not $appVersion) { [string]$appVersion = '' } + If (-not $appLang) { [string]$appLang = '' } + If (-not $appRevision) { [string]$appRevision = '' } + If (-not $appArch) { [string]$appArch = '' } } ## Sanitize the application details, as they can cause issues in the script @@ -10879,12 +11920,7 @@ Write-Log -Message "[$installName] setup started." -Source $appDeployToolkitName ## Assemblies: Load Try { - Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' - Add-Type -AssemblyName 'PresentationFramework' -ErrorAction 'Stop' - Add-Type -AssemblyName 'Microsoft.VisualBasic' -ErrorAction 'Stop' - Add-Type -AssemblyName 'System.Drawing' -ErrorAction 'Stop' - Add-Type -AssemblyName 'PresentationCore' -ErrorAction 'Stop' - Add-Type -AssemblyName 'WindowsBase' -ErrorAction 'Stop' + Add-Type -AssemblyName 'System.Drawing','System.Windows.Forms','PresentationFramework','Microsoft.VisualBasic','PresentationCore','WindowsBase' -ErrorAction 'Stop' } Catch { Write-Log -Message "Failed to load assembly. `n$(Resolve-Error)" -Severity 3 -Source $appDeployToolkitName @@ -10896,6 +11932,17 @@ Catch { } } +# Calculate banner height +[Int32]$appDeployLogoBannerHeight = 0 +try { + [System.Drawing.Bitmap]$appDeployLogoBannerObject = New-Object System.Drawing.Bitmap $appDeployLogoBanner + [Int32]$appDeployLogoBannerHeight = $appDeployLogoBannerObject.Height + if ($appDeployLogoBannerHeight -gt $appDeployLogoBannerMaxHeight) { + $appDeployLogoBannerHeight = $appDeployLogoBannerMaxHeight + } +} +catch { } + ## Check how the script was invoked If ($invokingScript) { Write-Log -Message "Script [$scriptPath] dot-source invoked by [$invokingScript]" -Source $appDeployToolkitName @@ -10925,6 +11972,8 @@ If ($configConfigVersion -lt $appDeployMainScriptMinimumConfigVersion) { ## Log system/script information If ($appScriptVersion) { Write-Log -Message "[$installName] script version is [$appScriptVersion]" -Source $appDeployToolkitName } +If ($appScriptDate) { Write-Log -Message "[$installName] script date is [$appScriptDate]" -Source $appDeployToolkitName } +If ($appScriptAuthor) { Write-Log -Message "[$installName] script author is [$appScriptAuthor]" -Source $appDeployToolkitName } If ($deployAppScriptFriendlyName) { Write-Log -Message "[$deployAppScriptFriendlyName] script version is [$deployAppScriptVersion]" -Source $appDeployToolkitName } If ($deployAppScriptParameters) { Write-Log -Message "The following non-default parameters were passed to [$deployAppScriptFriendlyName]: [$deployAppScriptParameters]" -Source $appDeployToolkitName } If ($appDeployMainScriptFriendlyName) { Write-Log -Message "[$appDeployMainScriptFriendlyName] script version is [$appDeployMainScriptVersion]" -Source $appDeployToolkitName } @@ -11090,24 +12139,30 @@ Catch { $runningTaskSequence = $false } - ## Check to see if the Task Scheduler service is in a healthy state by checking its services to see if they exist, are currently running, and have a start mode of 'Automatic'. ## The task scheduler service and the services it is dependent on can/should only be started/stopped/modified when running in the SYSTEM context. [boolean]$IsTaskSchedulerHealthy = $true If ($IsLocalSystemAccount) { - # Check the health of the 'COM+ Event System' service - [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'EventSystem' - # Check the health of the 'Remote Procedure Call (RPC)' service - [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'RpcSs' - # Check the health of the 'Windows Event Log' service - [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'EventLog' # Check the health of the 'Task Scheduler' service - [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'Schedule' - + Try { + If (Test-ServiceExists -Name 'Schedule' -ContinueOnError $false) { + If ((Get-ServiceStartMode -Name 'Schedule' -ContinueOnError $false) -ne 'Automatic') { + Set-ServiceStartMode -Name 'Schedule' -StartMode 'Automatic' -ContinueOnError $false + } + Start-ServiceAndDependencies -Name 'Schedule' -SkipServiceExistsTest -ContinueOnError $false + } + Else { + [boolean]$IsTaskSchedulerHealthy = $false + } + } + Catch { + [boolean]$IsTaskSchedulerHealthy = $false + } + # Log the health of the 'Task Scheduler' service Write-Log -Message "The task scheduler service is in a healthy state: $IsTaskSchedulerHealthy." -Source $appDeployToolkitName } Else { - Write-Log -Message "Skipping attempt to check for and make the task scheduler services healthy because the App Deployment Toolkit is not running under the [$LocalSystemNTAccount] account." -Source $appDeployToolkitName + Write-Log -Message "Skipping attempt to check for and make the task scheduler services healthy, because the App Deployment Toolkit is not running under the [$LocalSystemNTAccount] account." -Source $appDeployToolkitName } ## If script is running in session zero diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..f6f097c --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,7 @@ +# CHANGELOG + +2022-03-06 - Update PSADT to `3.8.4` from `3.8.3`. + Update Update Channel to `Current` from `SemiAnnual`. + +2021-01-17 - Update to new repository. + Update scripts to PowerShell Application Deployment Toolkit 3.8.3 from 3.8.2. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c6b9e95 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +# CONTRIBUTING diff --git a/Deploy-Application.example.ps1 b/Deploy-Application.example.ps1 new file mode 100644 index 0000000..b8f1ef3 --- /dev/null +++ b/Deploy-Application.example.ps1 @@ -0,0 +1,237 @@ +<# +.SYNOPSIS + This script performs the installation or uninstallation of an application(s). + # LICENSE # + PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. + Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . +.DESCRIPTION + The script is provided as a template to perform an install or uninstall of an application(s). + The script either performs an "Install" deployment type or an "Uninstall" deployment type. + The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. + The script dot-sources the AppDeployToolkitMain.ps1 script which contains the logic and functions required to install or uninstall an application. +.PARAMETER DeploymentType + The type of deployment to perform. Default is: Install. +.PARAMETER DeployMode + Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. +.PARAMETER AllowRebootPassThru + Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. +.PARAMETER TerminalServerMode + Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. +.PARAMETER DisableLogging + Disables logging to file for the script. Default is: $false. +.EXAMPLE + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" +.EXAMPLE + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -AllowRebootPassThru; Exit $LastExitCode }" +.EXAMPLE + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeploymentType 'Uninstall'; Exit $LastExitCode }" +.EXAMPLE + Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" +.NOTES + Toolkit Exit Code Ranges: + 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 + 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 + 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 +.LINK + http://psappdeploytoolkit.com +#> +[CmdletBinding()] +Param ( + [Parameter(Mandatory=$false)] + [ValidateSet('Install','Uninstall','Repair')] + [string]$DeploymentType = 'Install', + [Parameter(Mandatory=$false)] + [ValidateSet('Interactive','Silent','NonInteractive')] + [string]$DeployMode = 'Interactive', + [Parameter(Mandatory=$false)] + [switch]$AllowRebootPassThru = $false, + [Parameter(Mandatory=$false)] + [switch]$TerminalServerMode = $false, + [Parameter(Mandatory=$false)] + [switch]$DisableLogging = $false +) + +Try { + ## Set the script execution policy for this process + Try { Set-ExecutionPolicy -ExecutionPolicy 'ByPass' -Scope 'Process' -Force -ErrorAction 'Stop' } Catch {} + + ##*=============================================== + ##* VARIABLE DECLARATION + ##*=============================================== + ## Variables: Application + [string]$appVendor = '' + [string]$appName = '' + [string]$appVersion = '' + [string]$appArch = '' + [string]$appLang = 'EN' + [string]$appRevision = '01' + [string]$appScriptVersion = '1.0.0' + [string]$appScriptDate = 'XX/XX/20XX' + [string]$appScriptAuthor = '' + ##*=============================================== + ## Variables: Install Titles (Only set here to override defaults set by the toolkit) + [string]$installName = '' + [string]$installTitle = '' + + ##* Do not modify section below + #region DoNotModify + + ## Variables: Exit Code + [int32]$mainExitCode = 0 + + ## Variables: Script + [string]$deployAppScriptFriendlyName = 'Deploy Application' + [version]$deployAppScriptVersion = [version]'3.8.4' + [string]$deployAppScriptDate = '26/01/2021' + [hashtable]$deployAppScriptParameters = $psBoundParameters + + ## Variables: Environment + If (Test-Path -LiteralPath 'variable:HostInvocation') { $InvocationInfo = $HostInvocation } Else { $InvocationInfo = $MyInvocation } + [string]$scriptDirectory = Split-Path -Path $InvocationInfo.MyCommand.Definition -Parent + + ## Dot source the required App Deploy Toolkit Functions + Try { + [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" + If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } + If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } + } + Catch { + If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 } + Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' + ## Exit the script, returning the exit code to SCCM + If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } + } + + #endregion + ##* Do not modify section above + ##*=============================================== + ##* END VARIABLE DECLARATION + ##*=============================================== + + If ($deploymentType -ine 'Uninstall' -and $deploymentType -ine 'Repair') { + ##*=============================================== + ##* PRE-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Installation' + + ## Show Welcome Message, close Internet Explorer if required, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt + Show-InstallationWelcome -CloseApps 'iexplore' -AllowDefer -DeferTimes 3 -CheckDiskSpace -PersistPrompt + + ## Show Progress Message (with the default message) + Show-InstallationProgress + + ## + + + ##*=============================================== + ##* INSTALLATION + ##*=============================================== + [string]$installPhase = 'Installation' + + ## Handle Zero-Config MSI Installations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } + } + + ## + + + ##*=============================================== + ##* POST-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Installation' + + ## + + ## Display a message at the end of the install + If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } + } + ElseIf ($deploymentType -ieq 'Uninstall') + { + ##*=============================================== + ##* PRE-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Uninstallation' + + ## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing + Show-InstallationWelcome -CloseApps 'iexplore' -CloseAppsCountdown 60 + + ## Show Progress Message (with the default message) + Show-InstallationProgress + + ## + + + ##*=============================================== + ##* UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Uninstallation' + + ## Handle Zero-Config MSI Uninstallations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } + + # + + + ##*=============================================== + ##* POST-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Uninstallation' + + ## + + + } + ElseIf ($deploymentType -ieq 'Repair') + { + ##*=============================================== + ##* PRE-REPAIR + ##*=============================================== + [string]$installPhase = 'Pre-Repair' + + ## Show Progress Message (with the default message) + Show-InstallationProgress + + ## + + ##*=============================================== + ##* REPAIR + ##*=============================================== + [string]$installPhase = 'Repair' + + ## Handle Zero-Config MSI Repairs + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } + # + + ##*=============================================== + ##* POST-REPAIR + ##*=============================================== + [string]$installPhase = 'Post-Repair' + + ## + + + } + ##*=============================================== + ##* END SCRIPT BODY + ##*=============================================== + + ## Call the Exit-Script function to perform final cleanup operations + Exit-Script -ExitCode $mainExitCode +} +Catch { + [int32]$mainExitCode = 60001 + [string]$mainErrorMessage = "$(Resolve-Error)" + Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName + Show-DialogBox -Text $mainErrorMessage -Icon 'Stop' + Exit-Script -ExitCode $mainExitCode +} diff --git a/Deploy-Application.exe.config b/Deploy-Application.exe.config index b435f31..1da27ea 100644 --- a/Deploy-Application.exe.config +++ b/Deploy-Application.exe.config @@ -1,4 +1,4 @@ - + diff --git a/Deploy-Application.ps1 b/Deploy-Application.ps1 new file mode 100644 index 0000000..bbfa494 --- /dev/null +++ b/Deploy-Application.ps1 @@ -0,0 +1,234 @@ +<# +.SYNOPSIS + This script performs the installation or uninstallation of an application(s). + # LICENSE # + PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. + Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . +.DESCRIPTION + The script is provided as a template to perform an install or uninstall of an application(s). + The script either performs an "Install" deployment type or an "Uninstall" deployment type. + The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. + The script dot-sources the AppDeployToolkitMain.ps1 script which contains the logic and functions required to install or uninstall an application. +.PARAMETER DeploymentType + The type of deployment to perform. Default is: Install. +.PARAMETER DeployMode + Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. +.PARAMETER AllowRebootPassThru + Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. +.PARAMETER TerminalServerMode + Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. +.PARAMETER DisableLogging + Disables logging to file for the script. Default is: $false. +.EXAMPLE + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" +.EXAMPLE + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -AllowRebootPassThru; Exit $LastExitCode }" +.EXAMPLE + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeploymentType 'Uninstall'; Exit $LastExitCode }" +.EXAMPLE + Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" +.NOTES + Toolkit Exit Code Ranges: + 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 + 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 + 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 +.LINK + http://psappdeploytoolkit.com +#> +[CmdletBinding()] +Param ( + [Parameter(Mandatory=$false)] + [ValidateSet('Install','Uninstall','Repair')] + [string]$DeploymentType = 'Install', + [Parameter(Mandatory=$false)] + [ValidateSet('Interactive','Silent','NonInteractive')] + [string]$DeployMode = 'Interactive', + [Parameter(Mandatory=$false)] + [switch]$AllowRebootPassThru = $false, + [Parameter(Mandatory=$false)] + [switch]$TerminalServerMode = $false, + [Parameter(Mandatory=$false)] + [switch]$DisableLogging = $false +) + +Try { + ## Set the script execution policy for this process + Try { Set-ExecutionPolicy -ExecutionPolicy 'ByPass' -Scope 'Process' -Force -ErrorAction 'Stop' } Catch {} + + ##*=============================================== + ##* VARIABLE DECLARATION + ##*=============================================== + ## Variables: Application + [string]$appVendor = 'Microsoft Corporation' + [string]$appName = '365 Apps for Enterprise' + [string]$appVersion = '' # No need to display this! + [string]$appArch = 'x64' + [string]$appLang = 'EN' + [string]$appRevision = '01' + [string]$appScriptVersion = '1.3.0' + [string]$appScriptDate = '2022-03-06' + [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' + ##*=============================================== + ## Variables: Install Titles (Only set here to override defaults set by the toolkit) + [string]$installName = '' + [string]$installTitle = '' + + ##* Do not modify section below + #region DoNotModify + + ## Variables: Exit Code + [int32]$mainExitCode = 0 + + ## Variables: Script + [string]$deployAppScriptFriendlyName = 'Deploy Application' + [version]$deployAppScriptVersion = [version]'3.8.4' + [string]$deployAppScriptDate = '26/01/2021' + [hashtable]$deployAppScriptParameters = $psBoundParameters + + ## Variables: Environment + If (Test-Path -LiteralPath 'variable:HostInvocation') { $InvocationInfo = $HostInvocation } Else { $InvocationInfo = $MyInvocation } + [string]$scriptDirectory = Split-Path -Path $InvocationInfo.MyCommand.Definition -Parent + + ## Dot source the required App Deploy Toolkit Functions + Try { + [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" + If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } + If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } + } + Catch { + If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 } + Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' + ## Exit the script, returning the exit code to SCCM + If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } + } + + #endregion + ##* Do not modify section above + ##*=============================================== + ##* END VARIABLE DECLARATION + ##*=============================================== + + If ($deploymentType -ine 'Uninstall' -and $deploymentType -ine 'Repair') { + ##*=============================================== + ##* PRE-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Installation' + + ## Show Welcome Message, close Internet Explorer if required, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are installing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + + ## + + + ##*=============================================== + ##* INSTALLATION + ##*=============================================== + [string]$installPhase = 'Installation' + + ## Handle Zero-Config MSI Installations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } + } + + ## + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Install.xml`"" + + ##*=============================================== + ##* POST-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Installation' + + ## + + ## Display a message at the end of the install + #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } + } + ElseIf ($deploymentType -ieq 'Uninstall') + { + ##*=============================================== + ##* PRE-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Uninstallation' + + ## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are removing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + + ## + + ##*=============================================== + ##* UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Uninstallation' + + ## Handle Zero-Config MSI Uninstallations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } + + # + + ##*=============================================== + ##* POST-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Uninstallation' + + ## + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Uninstall.xml`"" + + } + ElseIf ($deploymentType -ieq 'Repair') + { + ##*=============================================== + ##* PRE-REPAIR + ##*=============================================== + [string]$installPhase = 'Pre-Repair' + + ## Show Progress Message (with the default message) + Show-InstallationProgress + + ## + + ##*=============================================== + ##* REPAIR + ##*=============================================== + [string]$installPhase = 'Repair' + + ## Handle Zero-Config MSI Repairs + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } + # + + ##*=============================================== + ##* POST-REPAIR + ##*=============================================== + [string]$installPhase = 'Post-Repair' + + ## + + } + ##*=============================================== + ##* END SCRIPT BODY + ##*=============================================== + + ## Call the Exit-Script function to perform final cleanup operations + Exit-Script -ExitCode $mainExitCode +} +Catch { + [int32]$mainExitCode = 60001 + [string]$mainErrorMessage = "$(Resolve-Error)" + Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName + Show-DialogBox -Text $mainErrorMessage -Icon 'Stop' + Exit-Script -ExitCode $mainExitCode +} diff --git a/Deploy-M365Apps.ps1 b/Deploy-M365Apps.ps1 index 3bc79b0..04b87db 100644 --- a/Deploy-M365Apps.ps1 +++ b/Deploy-M365Apps.ps1 @@ -22,13 +22,13 @@ .PARAMETER DisableLogging Disables logging to file for the script. Default is: $false. .EXAMPLE - powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" .EXAMPLE - powershell.exe -Command "& { & '.\Deploy-Application.ps1' -AllowRebootPassThru; Exit $LastExitCode }" + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -AllowRebootPassThru; Exit $LastExitCode }" .EXAMPLE - powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeploymentType 'Uninstall'; Exit $LastExitCode }" + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeploymentType 'Uninstall'; Exit $LastExitCode }" .EXAMPLE - Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" + Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" .NOTES Toolkit Exit Code Ranges: 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 @@ -61,15 +61,15 @@ Try { ##* VARIABLE DECLARATION ##*=============================================== ## Variables: Application - [string]$appVendor = 'Microsoft' - [string]$appName = 'M365 Apps' - [string]$appVersion = '' - [string]$appArch = 'x64' - [string]$appLang = 'EN' - [string]$appRevision = '01' - [string]$appScriptVersion = '1.0.0' - [string]$appScriptDate = '07/08/2020' - [string]$appScriptAuthor = 'Sandy Zeng' + [string]$appVendor = 'Microsoft Corporation' + [string]$appName = '365 Apps for Enterprise' + [string]$appVersion = '' # No need to display this! + [string]$appArch = 'x64' + [string]$appLang = 'EN' + [string]$appRevision = '01' + [string]$appScriptVersion = '1.3.0' + [string]$appScriptDate = '2022-03-06' + [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' ##*=============================================== ## Variables: Install Titles (Only set here to override defaults set by the toolkit) [string]$installName = '' @@ -83,8 +83,8 @@ Try { ## Variables: Script [string]$deployAppScriptFriendlyName = 'Deploy Application' - [version]$deployAppScriptVersion = [version]'3.8.2' - [string]$deployAppScriptDate = '08/05/2020' + [version]$deployAppScriptVersion = [version]'3.8.4' + [string]$deployAppScriptDate = '26/01/2021' [hashtable]$deployAppScriptParameters = $psBoundParameters ## Variables: Environment @@ -120,11 +120,10 @@ Try { Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are insalling $installTitle. Please wait!" -WindowLocation 'BottomRight' -TopMost $false - + Show-InstallationProgress -StatusMessage "We are installing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + ## - ##*=============================================== ##* INSTALLATION ##*=============================================== @@ -137,11 +136,7 @@ Try { } ## - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\M365Apps_Install.xml`"" - - ## Force update group policy - gpupdate /force /wait:0 - + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\M365Apps_Install.xml`"" ##*=============================================== ##* POST-INSTALLATION @@ -149,8 +144,6 @@ Try { [string]$installPhase = 'Post-Installation' ## - ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 ## Display a message at the end of the install #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } @@ -166,11 +159,10 @@ Try { Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are remvoing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - + Show-InstallationProgress -StatusMessage "We are removing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + ## - ##*=============================================== ##* UNINSTALLATION ##*=============================================== @@ -183,7 +175,6 @@ Try { } # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\M365Apps_Uninstall.xml`"" ##*=============================================== ##* POST-UNINSTALLATION @@ -191,7 +182,7 @@ Try { [string]$installPhase = 'Post-Uninstallation' ## - + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\M365Apps_Uninstall.xml`"" } ElseIf ($deploymentType -ieq 'Repair') @@ -214,7 +205,7 @@ Try { ## Handle Zero-Config MSI Repairs If ($useDefaultMsi) { [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat + Execute-MSI @ExecuteDefaultMSISplat } # @@ -225,8 +216,7 @@ Try { ## - - } + } ##*=============================================== ##* END SCRIPT BODY ##*=============================================== diff --git a/Deploy-Project.ps1 b/Deploy-Project.ps1 index 93dd11d..ebe5868 100644 --- a/Deploy-Project.ps1 +++ b/Deploy-Project.ps1 @@ -22,13 +22,13 @@ .PARAMETER DisableLogging Disables logging to file for the script. Default is: $false. .EXAMPLE - powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" .EXAMPLE - powershell.exe -Command "& { & '.\Deploy-Application.ps1' -AllowRebootPassThru; Exit $LastExitCode }" + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -AllowRebootPassThru; Exit $LastExitCode }" .EXAMPLE - powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeploymentType 'Uninstall'; Exit $LastExitCode }" + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeploymentType 'Uninstall'; Exit $LastExitCode }" .EXAMPLE - Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" + Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" .NOTES Toolkit Exit Code Ranges: 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 @@ -61,15 +61,15 @@ Try { ##* VARIABLE DECLARATION ##*=============================================== ## Variables: Application - [string]$appVendor = 'Microsoft' - [string]$appName = 'Project' - [string]$appVersion = '' - [string]$appArch = 'x64' - [string]$appLang = 'EN' - [string]$appRevision = '01' - [string]$appScriptVersion = '1.0.0' - [string]$appScriptDate = '07/08/2020' - [string]$appScriptAuthor = 'Sandy Zeng' + [string]$appVendor = 'Microsoft Corporation' + [string]$appName = 'Project' + [string]$appVersion = '' # No need to display this! + [string]$appArch = 'x64' + [string]$appLang = 'EN' + [string]$appRevision = '01' + [string]$appScriptVersion = '1.3.0' + [string]$appScriptDate = '2022-03-06' + [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' ##*=============================================== ## Variables: Install Titles (Only set here to override defaults set by the toolkit) [string]$installName = '' @@ -83,8 +83,8 @@ Try { ## Variables: Script [string]$deployAppScriptFriendlyName = 'Deploy Application' - [version]$deployAppScriptVersion = [version]'3.8.2' - [string]$deployAppScriptDate = '08/05/2020' + [version]$deployAppScriptVersion = [version]'3.8.4' + [string]$deployAppScriptDate = '26/01/2021' [hashtable]$deployAppScriptParameters = $psBoundParameters ## Variables: Environment @@ -120,8 +120,8 @@ Try { Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are insalling $installTitle. Please wait!" -WindowLocation 'BottomRight' -TopMost $false - + Show-InstallationProgress -StatusMessage "We are installing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + ## @@ -137,11 +137,7 @@ Try { } ## - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\Project_Install.xml`"" - - ## Force update group policy - gpupdate /force /wait:0 - + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Project_Install.xml`"" ##*=============================================== ##* POST-INSTALLATION @@ -149,8 +145,6 @@ Try { [string]$installPhase = 'Post-Installation' ## - ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 ## Display a message at the end of the install #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } @@ -166,11 +160,10 @@ Try { Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are remvoing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - + Show-InstallationProgress -StatusMessage "We are removing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + ## - ##*=============================================== ##* UNINSTALLATION ##*=============================================== @@ -183,7 +176,6 @@ Try { } # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\Project_Uninstall.xml`"" ##*=============================================== ##* POST-UNINSTALLATION @@ -191,7 +183,7 @@ Try { [string]$installPhase = 'Post-Uninstallation' ## - + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Project_Uninstall.xml`"" } ElseIf ($deploymentType -ieq 'Repair') @@ -214,7 +206,7 @@ Try { ## Handle Zero-Config MSI Repairs If ($useDefaultMsi) { [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat + Execute-MSI @ExecuteDefaultMSISplat } # @@ -225,8 +217,7 @@ Try { ## - - } + } ##*=============================================== ##* END SCRIPT BODY ##*=============================================== diff --git a/Deploy-Visio.ps1 b/Deploy-Visio.ps1 index 7293e5a..44c47cd 100644 --- a/Deploy-Visio.ps1 +++ b/Deploy-Visio.ps1 @@ -22,13 +22,13 @@ .PARAMETER DisableLogging Disables logging to file for the script. Default is: $false. .EXAMPLE - powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" .EXAMPLE - powershell.exe -Command "& { & '.\Deploy-Application.ps1' -AllowRebootPassThru; Exit $LastExitCode }" + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -AllowRebootPassThru; Exit $LastExitCode }" .EXAMPLE - powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeploymentType 'Uninstall'; Exit $LastExitCode }" + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeploymentType 'Uninstall'; Exit $LastExitCode }" .EXAMPLE - Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" + Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" .NOTES Toolkit Exit Code Ranges: 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 @@ -61,15 +61,15 @@ Try { ##* VARIABLE DECLARATION ##*=============================================== ## Variables: Application - [string]$appVendor = 'Microsoft' - [string]$appName = 'Visio' - [string]$appVersion = '' - [string]$appArch = 'x64' - [string]$appLang = 'EN' - [string]$appRevision = '01' - [string]$appScriptVersion = '1.0.0' - [string]$appScriptDate = '07/08/2020' - [string]$appScriptAuthor = 'Sandy Zeng' + [string]$appVendor = 'Microsoft Corporation' + [string]$appName = 'Visio' + [string]$appVersion = '' # No need to display this! + [string]$appArch = 'x64' + [string]$appLang = 'EN' + [string]$appRevision = '01' + [string]$appScriptVersion = '1.3.0' + [string]$appScriptDate = '2022-03-06' + [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' ##*=============================================== ## Variables: Install Titles (Only set here to override defaults set by the toolkit) [string]$installName = '' @@ -83,8 +83,8 @@ Try { ## Variables: Script [string]$deployAppScriptFriendlyName = 'Deploy Application' - [version]$deployAppScriptVersion = [version]'3.8.2' - [string]$deployAppScriptDate = '08/05/2020' + [version]$deployAppScriptVersion = [version]'3.8.4' + [string]$deployAppScriptDate = '26/01/2021' [hashtable]$deployAppScriptParameters = $psBoundParameters ## Variables: Environment @@ -120,8 +120,8 @@ Try { Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are insalling $installTitle. Please wait!" -WindowLocation 'BottomRight' -TopMost $false - + Show-InstallationProgress -StatusMessage "We are installing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + ## @@ -137,11 +137,7 @@ Try { } ## - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\Visio_Install.xml`"" - - ## Force update group policy - gpupdate /force /wait:0 - + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Visio_Install.xml`"" ##*=============================================== ##* POST-INSTALLATION @@ -149,8 +145,6 @@ Try { [string]$installPhase = 'Post-Installation' ## - ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 ## Display a message at the end of the install #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } @@ -166,11 +160,10 @@ Try { Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are remvoing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - + Show-InstallationProgress -StatusMessage "We are removing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + ## - ##*=============================================== ##* UNINSTALLATION ##*=============================================== @@ -183,7 +176,6 @@ Try { } # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\Visio_Uninstall.xml`"" ##*=============================================== ##* POST-UNINSTALLATION @@ -191,7 +183,7 @@ Try { [string]$installPhase = 'Post-Uninstallation' ## - + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Visio_Uninstall.xml`"" } ElseIf ($deploymentType -ieq 'Repair') @@ -214,7 +206,7 @@ Try { ## Handle Zero-Config MSI Repairs If ($useDefaultMsi) { [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat + Execute-MSI @ExecuteDefaultMSISplat } # @@ -225,8 +217,7 @@ Try { ## - - } + } ##*=============================================== ##* END SCRIPT BODY ##*=============================================== diff --git a/Files/M365Apps_Uninstall.xml b/Files/M365Apps_Uninstall.xml deleted file mode 100644 index 27f614d..0000000 --- a/Files/M365Apps_Uninstall.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Files/Project_Uninstall.xml b/Files/Project_Uninstall.xml deleted file mode 100644 index 48badf7..0000000 --- a/Files/Project_Uninstall.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Files/Visio_Uninstall.xml b/Files/Visio_Uninstall.xml deleted file mode 100644 index 35e4de1..0000000 --- a/Files/Visio_Uninstall.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Files/setup.exe b/Files/setup.exe index f91bbe6..fdb53ae 100644 Binary files a/Files/setup.exe and b/Files/setup.exe differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..106e867 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# DeployM365Apps + +Deploy the Microsoft 365 Apps for Enterpirse via Microsoft CDN with PowerShell Application Deployment Toolkit 3.8.4. diff --git a/Files/M365Apps_Install.xml b/SupportFiles/M365Apps_Install.xml similarity index 66% rename from Files/M365Apps_Install.xml rename to SupportFiles/M365Apps_Install.xml index 91f2530..26d5f9d 100644 --- a/Files/M365Apps_Install.xml +++ b/SupportFiles/M365Apps_Install.xml @@ -1,38 +1,37 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SupportFiles/M365Apps_Uninstall.xml b/SupportFiles/M365Apps_Uninstall.xml new file mode 100644 index 0000000..5941fb4 --- /dev/null +++ b/SupportFiles/M365Apps_Uninstall.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Files/Project_Install.xml b/SupportFiles/Project_Install.xml similarity index 77% rename from Files/Project_Install.xml rename to SupportFiles/Project_Install.xml index d4d2d44..010678b 100644 --- a/Files/Project_Install.xml +++ b/SupportFiles/Project_Install.xml @@ -1,28 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SupportFiles/Project_Uninstall.xml b/SupportFiles/Project_Uninstall.xml new file mode 100644 index 0000000..bb5c793 --- /dev/null +++ b/SupportFiles/Project_Uninstall.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Files/Visio_Install.xml b/SupportFiles/Visio_Install.xml similarity index 81% rename from Files/Visio_Install.xml rename to SupportFiles/Visio_Install.xml index 950d4eb..f87bfe9 100644 --- a/Files/Visio_Install.xml +++ b/SupportFiles/Visio_Install.xml @@ -1,32 +1,29 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SupportFiles/Visio_Uninstall.xml b/SupportFiles/Visio_Uninstall.xml new file mode 100644 index 0000000..737983f --- /dev/null +++ b/SupportFiles/Visio_Uninstall.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file