-
Notifications
You must be signed in to change notification settings - Fork 45
/
Copy pathTrimmer.ps1
1170 lines (991 loc) · 48.9 KB
/
Trimmer.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#requires -version 3
<#
Trim working sets of processes or set maximum working set sizes
Guy Leech, 2018
Modification History
10/03/18 GL Optimised code
12/03/18 GL Added reporting
16/03/18 GL Fixed bug where -available was being passed as False when not specified
Workaround for invocation external to PowerShell for arrays being flattened
Made process ids parameter an array
Added process id and name filter to Get-Process cmdlet call for efficiency
Added include and exclude options for user names
30/03/18 GL Added ability to wait for specific list of processes to start before continuing.
Added looping
23/04/18 GL Added exiting from loop if pids specified no longer exist
Added -forceIt as equivalent to -confirm:$false for use via scheduled tasks
12/02/22 GL Added output of whether hard workin set limits and -nogridview
#>
<#
.SYNOPSIS
Manipulate the working sets (memory usage) of processes or report their current memory usage and working set limits and types (hard or soft)
.DESCRIPTION
Can reduce the memory footprints of running processes to make more memory available and stop processes that leak memory from leaking
.PARAMETER Processes
A comma separated list of process names to use (without the .exe extension). By default all processes will be trimmed if the script has access to them.
.PARAMETER IncludeUsers
A comma separated of qualified names of process owners to include. Must be run as an admin for this to work. Specify domain or other qualifier, e.g. "NT AUTHORITY\SYSTEM'
.PARAMETER ExcludeUsers
A comma separated of qualified names of process owners to exclude. Must be run as an admin for this to work. Specify domain or other qualifier, e.g. "NT AUTHORITY\NETWORK SERVICE' or DOMAIN\Chris.Harvey
.PARAMETER Exclude
A comma separated list of process names to ignore (without the .exe extension).
.PARAMETER Above
Only trim the working set if the process' working set is currently above this value. Qualify with MB or GB as required. Default is to trim all processes
.PARAMETER WaitFor
A comma separated list of processes to wait for unless -alreadyStarted is specified and one of the processes is already running unless it is not in the current session and -thisSession specified
.PARAMETER AlreadyStarted
Used when -WaitFor specified such that waiting will not occur if any of the processes specified via -WaitFor are already running although only in the current session if -thisSession is specified
.PARAMETER PollPeriod
The time in seconds between checks for new processes that match the -WaitFor process list.
.PARAMETER MinWorkingSet
Set the minimum working set size to this value. Qualify with MB or GB as required. Default is to not set a minimum value.
.PARAMETER MaxWorkingSet
Set the maximum working set size to this value. Qualify with MB or GB as required. Default is to not set a maximum value.
.PARAMETER HardMin
When MinWorkingSet is specified, the limit will be enforced so the working set is never allowed to be less that the value. Default is a soft limit which is not enforced.
.PARAMETER HardMax
When MaxWorkingSet is specified, the limit will be enforced so the working set is never allowed to exceed the value. Default is a soft limit which can be exceeded.
.PARAMETER Loop
Loop infinitely
.PARAMETER forceIt
DO not prompt for confirmation before adjusting CPU priority
.PARAMETER Report
Produce a report of the current working set usage and limit types for processes in the selection. Will output to a grid view unless -outputFile is specified.
.PARAMETER OutputFile
Ue with -report to write the results to a csv format file. If the file already exists the operation will fail.
.PARAMETER ProcessIds
Only trim the specific process ids
.PARAMETER ThisSession
Will only trim working sets of processes in the same session as the sript is running in. The default is to trim in all sessions.
.PARAMETER SessionIds
Only trim processes running in the specified sessions which is a comma separated list of session ids. The default is to trim in all sessions.
.PARAMETER NotSessionId
Only trim processes not running in the specified sessions which is a comma separated list of session ids. The default is to trim in all sessions.
.PARAMETER Available
Specify as a percentage or an absolute value. Will only trim if the available memory is below the parameter specified. The default is to always trim.
.PARAMETER Savings
This will show a summary of the trimming at the end of processing. Note that working sets can grow once trimmed so the amount trimmed may be higher than the actual increase in available memory.
.PARAMETER Disconnected
This will only trim memory in sessions which are disconnected. The default is to target all sessions.
.PARAMETER Idle
If no user input has been received in the last x seconds, whre x is the parameter passed, then the session is considered idle and processes will be trimmed.
.PARAMETER nogridview
Put the results (use -report) onto the pipeline, not in a grid view
.PARAMETER Background
Only trim processes which are not the process responsible for the foreground window. Implies -ThisSession since cannot check windows in other sessions.
.PARAMETER Install
Create two scheduled tasks, one which will trim that user's session on disconnect or screen lock and the other runs at the frequency specified in seconds divided by two, checks if the user is idle for the specified number of seconds and trims if they are.
So if a parameter of 600 is passed, the task will run every 5 minutes and if the user has made no mouse or keyboard input for 10 minutes then their processes are trimmed.
.PARAMETER Uninstall
Removes the two scheduled tasks previously created for the user running the script.
.EXAMPLE
& .\Trimmer.ps1
This will trim all processes in all sessions to which the account running the script has access.
.EXAMPLE
& .\Trimmer.ps1 -ThisSession -Above 50MB
Only trim processes in the same session as the script and whose working set exceeds 50MB.
.EXAMPLE
& .\Trimmer.ps1 -MaxWorkingSet 100MB -HardMax -Above 50MB -Processes LeakyApp
Only trim processes called LeakyApp in any session whose working set exceeds 50MB and set the maximum working set size to 100MB which cannot be exceeded.
Will only apply to instances of LeakyApp which have already started, instances started after the script is run will not be subject to the restriction.
.EXAMPLE
& .\Trimmer.ps1 -MaxWorkingSet 10MB -Processes Chrome
Trim Chrome processes to 10MB, rather than completely emptying their working set. If processes rapidly regain working sets after being trimmed, this can
cause page file thrashing so reducing the working set but not completely emptying them can still save memory but reduce the risk of page file thrashing.
Picking the figure to use for the working set is trial and error but typically one would use the value that it settles to a few minutes after trimming.
.EXAMPLE
& .\Trimmer.ps1 -Install 600 -Logoff
Create two scheduled tasks for this user which only run when the user is logged on. The first runs at session lock or disconnect and trims all processes in that session.
The second task runs every 300 seconds and if no mouse or keyboard input has been received in the last 600 seconds then all background processes in that session will be trimmed.
At logoff, the scheduled tasks will be removed.
.EXAMPLE
& .\Trimmer.ps1 Uninstall
Delete the two scheduled tasks for this user
.NOTES
If you trim too much and/or too frequently, you run the risk of reducing performance by overusing the page file.
Supports the "-whatif" parameter so you can see what processes it will trim without actually performing the trim.
If emptying the working set does cause too much paging, try using the -MaxWorkingSet parameter to apply a soft limit which will cause the process
to be trimmed down to that value but it can then grow larger if required.
Uses Windows API SetProcessWorkingSetSizeEx() - https://msdn.microsoft.com/en-us/library/windows/desktop/ms686237(v=vs.85).aspx
#>
[cmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')]
Param
(
[string]$logFile ,
[int]$install ,
[int]$idle , ## seconds
[switch]$uninstall ,
[string[]]$processes ,
[string[]]$exclude ,
[string[]]$includeUsers ,
[string[]]$excludeUsers ,
[string[]]$waitFor ,
[switch]$alreadyStarted ,
[switch]$report ,
[switch]$nogridview ,
[string]$outputFile ,
[int]$above = 10MB ,
[int]$minWorkingSet = -1 ,
[int]$maxWorkingSet = -1 ,
[int]$pollPeriod = 5 ,
[switch]$hardMax ,
[switch]$hardMin ,
[switch]$newOnly ,
[switch]$thisSession ,
[string[]]$processIds ,
[string[]]$sessionIds ,
[string[]]$notSessionIds ,
[string]$available ,
[switch]$loop ,
[switch]$savings ,
[switch]$disconnected ,
[switch]$background ,
[switch]$scheduled ,
[switch]$logoff ,
[switch]$forceIt ,
[string]$taskFolder = '\MemoryTrimming'
)
[int]$minimumIdlePeriod = 120 ## where minimum reptition of a scheduled task must be at least 1 minute, thus idle time must be at least double that (https://msdn.microsoft.com/en-us/library/windows/desktop/aa382993(v=vs.85).aspx)
## Borrowed from http://stackoverflow.com/a/15846912 and adapted
Add-Type @'
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace PInvoke.Win32
{
public static class Memory
{
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool SetProcessWorkingSetSizeEx( IntPtr proc, int min, int max , int flags );
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool GetProcessWorkingSetSizeEx( IntPtr hProcess, ref int min, ref int max , ref int flags );
}
public static class UserInput
{
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
[DllImport("user32.dll", SetLastError=false)]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO
{
public uint cbSize;
public int dwTime;
}
public static DateTime LastInput
{
get
{
DateTime bootTime = DateTime.UtcNow.AddMilliseconds(-Environment.TickCount);
DateTime lastInput = bootTime.AddMilliseconds(LastInputTicks);
return lastInput;
}
}
public static TimeSpan IdleTime
{
get
{
return DateTime.UtcNow.Subtract(LastInput);
}
}
public static int LastInputTicks
{
get
{
LASTINPUTINFO lii = new LASTINPUTINFO();
lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
GetLastInputInfo(ref lii);
return lii.dwTime;
}
}
}
}
'@
Function Schedule-Task
{
Param
(
[string]$taskFolder ,
[string]$taskname ,
[string]$script , ## if null then we are deleting
[int]$idle ,
[switch]$background ,
[int]$above ,
[switch]$savings ,
[string]$available = $null,
[string[]]$processes ,
[string[]]$exclude ,
[string]$logFile = $null
)
Write-Verbose "Schedule-Task( $taskFolder , $taskName , $script )"
## https://www.experts-exchange.com/articles/11591/VBScript-and-Task-Scheduler-2-0-Creating-Scheduled-Tasks.html
Set-Variable TASK_LOGON_INTERACTIVE_TOKEN 3 #-Option Constant
Set-Variable TASK_RUNLEVEL_LUA 0 #-Option Constant
Set-Variable TASK_TRIGGER_EVENT 0 #-Option Constant
Set-Variable TASK_TRIGGER_TIME 1
Set-Variable TASK_TRIGGER_DAILY 2
Set-Variable TASK_TRIGGER_IDLE 6
Set-Variable TASK_TRIGGER_SESSION_STATE_CHANGE 11 #-Option Constant
Set-Variable TASK_STATE_SESSION_LOCK 7 #-Option Constant
Set-Variable TASK_STATE_REMOTE_DISCONNECT 4
Set-Variable TASK_ACTION_EXEC 0 #-Option Constant
Set-Variable TASK_CREATE_OR_UPDATE 6 #-Option Constant
$objTaskService = New-Object -ComObject "Schedule.Service" ##-Strict
$objTaskService.Connect()
$objRootFolder = $objTaskService.GetFolder("\")
$objTaskFolders = $objRootFolder.GetFolders(0)
[bool]$blnFoundTask = $false
ForEach( $objTaskFolder In $objTaskFolders )
{
If( $objTaskFolder.Path -eq $taskFolder )
{
$blnFoundTask = $True
break
}
}
if( [string]::IsNullOrEmpty( $script ) )
{
## Find task and delete
if( $blnFoundTask )
{
[bool]$deleted = $false
$objTaskFolder.GetTasks(0) | ?{ $_.Name -eq $taskname } | %{ $objTaskFolder.DeleteTask( $_.Name , 0 ) ; $deleted = $true }
if( ! $deleted )
{
Write-Warning "Failed to find task `"$taskname`" so cannot remove it"
}
}
else
{
Write-Warning "Unable to find task folder $taskFolder so cannot remove scheduled tasks"
}
return
}
elseif( ! $blnFoundTask )
{
$objTaskFolder = $objRootFolder.CreateFolder($taskFolder)
}
$objNewTaskDefinition = $objTaskService.NewTask(0)
$objNewTaskDefinition.Data = 'This is Guys task from PoSH'
$objNewTaskDefinition.RegistrationInfo.Author = $objTaskService.ConnectedDomain + "\" + $objTaskService.ConnectedUser
$objNewTaskDefinition.RegistrationInfo.Date = ([datetime]::Now).ToString("yyyy-MM-dd'T'HH:mm:ss")
$objNewTaskDefinition.RegistrationInfo.Description = 'Trim process memory'
$objNewTaskDefinition.RegistrationInfo.Documentation = 'RTFM'
$objNewTaskDefinition.RegistrationInfo.Source = 'PowerShell'
$objNewTaskDefinition.RegistrationInfo.URI = 'http://guyrleech.wordpress.com'
$objNewTaskDefinition.RegistrationInfo.Version = '1.0'
$objNewTaskDefinition.Principal.Id = 'My ID'
$objNewTaskDefinition.Principal.DisplayName = 'Principal Description'
$objNewTaskDefinition.Principal.UserId = $objTaskService.ConnectedDomain + "\" + $objTaskService.ConnectedUser
$objNewTaskDefinition.Principal.LogonType = $TASK_LOGON_INTERACTIVE_TOKEN
$objNewTaskDefinition.Principal.RunLevel = $TASK_RUNLEVEL_LUA
$objTaskTriggers = $objNewTaskDefinition.Triggers
$objTaskAction = $objNewTaskDefinition.Actions.Create($TASK_ACTION_EXEC)
$objTaskAction.Id = 'Execute Action'
## powershell.exe even with windowstyle hidden still shows a window so we start via vbs in order for it to be truly hidden
$vbsscriptbody = @"
Dim objShell,strArgs , i
for i = 0 to WScript.Arguments.length - 1
strArgs = strArgs & WScript.Arguments(i) & " "
next
Set objShell = CreateObject("WScript.Shell")
objShell.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ""$script"" -ThisSession -scheduled " & strArgs , 0
"@
[string]$vbsscript = $script -replace '\.ps1$' , '.vbs'
if( Test-Path $vbsscript )
{
[string]$content = ""
$existingScript = Get-Content $vbsscript | %{ $content += $_ + "`r`n" }
if( $content -ne $vbsscriptbody )
{
Write-Error "vbs script `"$vbsscript`" already exists but is different to the file we need to write"
}
}
else
{
[io.file]::WriteAllText( $vbsscript , $vbsscriptbody ) ## ensure no newline as breaks comparison
if( ! $? -or ! ( Test-Path $vbsscript ) )
{
Write-Error "Error creating vbs script `"$vbsscript`""
}
}
$objTaskAction.WorkingDirectory = $env:TEMP
$objTaskAction.Path = 'wscript.exe'
$objTaskAction.Arguments = "//nologo `"$vbsscript`""
if( $idle -gt 0 )
{
$objTaskAction.Arguments += " -Idle $install"
}
if( $background )
{
$objTaskAction.Arguments += ' -background'
}
if( $above -ge 0 )
{
$objTaskAction.Arguments += " -above $above"
}
if( $savings )
{
$objTaskAction.Arguments += " -savings"
}
if( ! [string]::IsNullOrEmpty( $available ) )
{
$objTaskAction.Arguments += " -available $available"
}
if( ! [string]::IsNullOrEmpty( $logFile ) )
{
$objTaskAction.Arguments += " -logfile `"$logfile`""
}
if( $processes -and $processes.Count )
{
$objTaskAction.Arguments += " -processes $processes"
}
if( $exclude -and $exclude.Count )
{
$objTaskAction.Arguments += " -exclude $exclude"
}
if( $VerbosePreference -eq 'Continue' )
{
$objTaskAction.Arguments += " -verbose"
}
## http://msdn.microsoft.com/en-us/library/windows/desktop/aa383480%28v=vs.85%29.aspx
$objNewTaskDefinition.Settings.Enabled = $true
$objNewTaskDefinition.Settings.Compatibility = 2 ## Win7/WS08R2
$objNewTaskDefinition.Settings.Priority = 5 ## 0 High - 10 Low
$objNewTaskDefinition.Settings.Hidden = $false
## Can't use idle trigger as means more than just no input from user so we run a standard, repeating scheduled task and check for no input in the script itself
if( $idle -gt 0 )
{
$objTaskTrigger = $objTaskTriggers.Create($TASK_TRIGGER_DAILY)
$objTaskTrigger.Enabled = $true
$objTaskTrigger.DaysInterval = 1
$objTaskTrigger.Repetition.Duration = 'P1D'
$objTaskTrigger.Repetition.Interval = 'PT' + [math]::Round( $idle / 2 ) + 'S'
$objTaskTrigger.Repetition.StopAtDurationEnd = $true
$objTaskTrigger.StartBoundary = ([datetime]::Now).ToString('yyyy-MM-dd''T''HH:mm:ss')
}
else
{
$objTaskTrigger = $objTaskTriggers.Create($TASK_TRIGGER_SESSION_STATE_CHANGE)
$objTaskTrigger.Enabled = $true
$objTaskTrigger.Id = 'Session state change lock'
$objTaskTrigger.StateChange = $TASK_STATE_SESSION_LOCK
## Format For Days = P#D where # is the number of days
## Format for Time = PT#[HMS] Where # is the duration and H for hours, M for minutes, S for seconds
$objTaskTrigger.ExecutionTimeLimit = 'PT5M'
$objTaskTrigger.Delay = 'PT5S'
$objTaskTrigger.UserId = $objTaskService.ConnectedDomain + '\' + $objTaskService.ConnectedUser
## http://msdn.microsoft.com/en-us/library/windows/desktop/aa382144%28v=vs.85%29.aspx
$objTaskTrigger = $objTaskTriggers.Create($TASK_TRIGGER_SESSION_STATE_CHANGE)
$objTaskTrigger.Enabled = $true
$objTaskTrigger.Id = 'Session state change disconnect'
$objTaskTrigger.StateChange = $TASK_STATE_REMOTE_DISCONNECT
## Format For Days = P#D where # is the number of days
## Format for Time = PT#[HMS] Where # is the duration and H for hours, M for minutes, S for seconds
$objTaskTrigger.ExecutionTimeLimit = "PT5M"
$objTaskTrigger.Delay = "PT5S"
$objTaskTrigger.UserId = $objTaskService.ConnectedDomain + '\' + $objTaskService.ConnectedUser
}
$objNewTaskDefinition.Settings.DisallowStartIfOnBatteries = $false
$objNewTaskDefinition.Settings.AllowDemandStart = $true
$objNewTaskDefinition.Settings.StartWhenAvailable = $true
$objNewTaskDefinition.Settings.RestartInterval = 'PT10M'
$objNewTaskDefinition.Settings.RestartCount = 2
$objNewTaskDefinition.Settings.ExecutionTimeLimit = "PT1H"
$objNewTaskDefinition.Settings.AllowHardTerminate = $true
## 0 = Run a second instance now (Parallel)
## 1 = Put the new instance in line behind the current running instance (Add To Queue)
## 2 = Ignore the new request
$objNewTaskDefinition.Settings.MultipleInstances = 2
try
{
$task = $objTaskFolder.RegisterTaskDefinition( $taskname , $objNewTaskDefinition , $TASK_CREATE_OR_UPDATE , $null , $null , $TASK_LOGON_INTERACTIVE_TOKEN )
}
catch
{
$task = $null
}
if( ! $task )
{
Write-Error ( "Failed to create scheduled task: {0}" -f $error[0] )
}
}
if( ! [string]::IsNullOrEmpty( $logFile ) )
{
Start-Transcript $logFile -Append
}
if( $install -gt 0 -or $uninstall )
{
Write-Verbose ( "{0} requested" -f $( if( $uninstall ) { "Uninstall" } else {"Install"} ) )
if( $uninstall -and $install -gt 0 )
{
Write-Error "Cannot specify -install and -uninstall together"
return 1
}
elseif( $report )
{
Write-Error "Cannot specify -install or -uninstall with -report"
}
elseif( $uninstall )
{
$scriptName = $null
}
elseif( $install -lt $minimumIdlePeriod ) ## minimum repetition is 1 minute see https://msdn.microsoft.com/en-us/library/windows/desktop/aa382993(v=vs.85).aspx
{
Write-Error "Idle time is too low - minimum idle time is $minimumIdlePeriod seconds"
return
}
else
{
$scriptName = & { $myInvocation.ScriptName }
}
[hashtable]$taskArguments =
@{
Taskfolder = $taskFolder
Script = $scriptName
Above = $above
Savings = $savings
Exclude = $exclude
Processes = $processes
Logfile = $logFile
Available = $available
}
Schedule-Task -taskName "Trim on lock and disconnect for $($env:username)" @taskArguments
Schedule-Task -taskName "Trim idle for $($env:username)" @taskArguments -idle $install -background $background
## if we have been asked to hook logoff, to uninstall, then we create a hidden window so we can capture events
if( $logoff )
{
Add-Type –AssemblyName System.Windows.Forms
$form = New-Object Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(0,0)
$form.Location = New-Object System.Drawing.Point(-5000,-5000)
$form.FormBorderStyle = 'FixedToolWindow'
$form.StartPosition = 'manual'
$form.ShowInTaskbar = $false
$form.WindowState = 'Normal'
$form.Visible = $false
$form.AutoSize = $false
$form.add_FormClosing(
{
Write-Verbose "$(Get-Date) dialog closing"
Schedule-Task -taskName "Trim on lock and disconnect for $($env:username)" -taskFolder $taskFolder -Script $null
Schedule-Task -taskName "Trim idle for $($env:username)" -taskFolder $taskFolder -Script $null
})
$form.add_Load({ $form.Opacity = 0 })
$form.add_Shown({ $form.Opacity = 100 })
Write-Verbose "About to show dialog for logoff intercept - script will not exit until logoff"
[void]$form.ShowDialog()
## We will only get here when the hidden dialogue exits which should only be logoff
}
if( ! [string]::IsNullOrEmpty( $logFile ) )
{
Stop-Transcript
}
Exit 0
}
[datetime]$monitoringStartTime = Get-Date
[int]$thisSessionId = (Get-Process -Id $pid).SessionId
## workaround for scheduled task not liking -confirm:$false being passed
if( $forceIt )
{
$ConfirmPreference = 'None'
}
do
{
if( $waitFor -and $waitFor.Count )
{
[bool]$found = $false
$thisProcess = $null
[datetime]$startedAfter = Get-Date
if( $alreadyStarted ) ## we are not waiting for new instances so existing ones qualify too
{
$startedAfter = Get-Date -Date '01/01/1970' ## saves having to grab LastBootupTime
}
while( ! $found )
{
Write-Verbose "$(Get-Date): waiting for one of $($waitFor -join ',') to launch (only in session $thisSessionId is $thisSession)"
## wait for one of a set of specific processes to start - useful when you need to apply a hard working set limit to a known leaky process
Get-Process -Name $waitFor -ErrorAction SilentlyContinue | Where-Object { $_.StartTime -gt $startedAfter } | ForEach-Object `
{
if( ! $found )
{
$thisProcess = $_
## we don't support all filtering options here
if( $thisSession )
{
$found = ( $thisSessionId -eq $thisProcess.SessionId )
}
else
{
$found = $true
}
}
}
if( ! $found )
{
Start-Sleep -Seconds $pollPeriod
}
}
Write-Verbose "$(Get-Date) : process $($thisProcess.Name) id $($thisProcess.Id) started at $($thisProcess.StartTime)"
}
if( $idle -gt 0 )
{
$idleTime = [PInvoke.Win32.UserInput]::IdleTime.TotalSeconds
Write-Verbose "Idle time is $idleTime seconds"
if( $idleTime -lt $idle )
{
Write-Verbose "Idle time is only $idleTime seconds, less than $idle"
if( ! $scheduled -or ( $scheduled -and ! $background ) )
{
if( ! [string]::IsNullOrEmpty( $logFile ) )
{
Stop-Transcript
}
return
}
else
{
Write-Verbose "Not idle but we are a scheduled task and trimming background processes so continue"
}
}
}
[long]$ActiveHandle = $null
$activePid = [IntPtr]::Zero
if( $background )
{
[long]$ActiveHandle = [PInvoke.Win32.UserInput]::GetForeGroundWindow( )
if( ! $ActiveHandle )
{
Write-Error "Unable to find foreground window"
return 1
}
else
{
$activeThreadId = [PInvoke.Win32.UserInput]::GetWindowThreadProcessId( $ActiveHandle , [ref] $activePid )
if( $activePid -ne [IntPtr]::Zero )
{
Write-Verbose ( "Foreground window is pid {0} {1}" -f $activePid , (Get-Process -Id $activePid).Name )
}
else
{
Write-Error "Unable to get handle on process for foreground window $ActiveHandle"
return 1
}
}
$thisSession = $true ## can only check windows in this session
}
[int]$flags = 0
if( $minWorkingSet -gt 0 )
{
if( $hardMin )
{
$flags = $flags -bor 1
}
else ## soft
{
$flags = $flags -bor 2
}
}
if( $maxWorkingSet -gt 0 )
{
if( $hardMax )
{
$flags = $flags -bor 4
}
else ## soft
{
$flags = $flags -bor 8
}
if( $minWorkingSet -le 0 )
{
$minWorkingSet = 1 ## if a maximum is specified then we must specify a minimum too - this will default to the minimum
}
}
[long]$availableMemory = (Get-Counter '\Memory\Available MBytes').CounterSamples[0].CookedValue * 1MB
if( ! [string]::IsNullOrEmpty( $available ) )
{
## Need to find out memory available and total
[long]$totalMemory = ( Get-CimInstance -Class Win32_ComputerSystem -Property TotalPhysicalMemory ).TotalPhysicalMemory
[int]$left = ( $availableMemory / $totalMemory ) * 100
Write-Verbose ( "Available memory is {0}MB out of {1}MB total ({2}%)" -f ( $availableMemory / 1MB ) , [math]::Floor( $totalMemory / 1MB ) , [math]::Round( $left ) )
[bool]$proceed = $false
## See if we are dealing with absolute or percentage
if( $available[-1] -eq '%' )
{
[int]$percentage = $available -replace '%$'
$proceed = $left -lt $percentage
}
else ## absolute
{
[long]$threshold = Invoke-Expression $available
$proceed = $availableMemory -lt $threshold
}
if( ! $proceed )
{
Write-Verbose "Not trimming as memory available is above specified threshold"
if( ! [string]::IsNullOrEmpty( $logFile ) )
{
Stop-Transcript
}
Exit 0
}
}
[long]$saved = 0
[int]$trimmed = 0
$params = @{}
[int[]]$sessionsToTarget = @()
$results = New-Object -TypeName System.Collections.ArrayList
if( $disconnected )
{
## no native session support so parse output of quser.exe
## Columns are 'USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME' but SESSIONNAME is empty for disconnected so all shifted left by one column (yuck!)
$sessionsToTarget = @( (quser) -replace '\s{2,}', ',' | ConvertFrom-Csv | ForEach-Object `
{
$session = $_
if( $session.Id -like "Disc*" )
{
$session.SessionName -as [int]
Write-Verbose ( "Session {0} is disconnected for user {1} logon {2} idle {3}" -f $session.SessionName , $session.Username , $session.'Idle Time' , $session.State )
}
} )
}
## Reform arrays as they will not be passed correctly if command not invoked natively in PowerShell, e.g. via cmd or scheduled task
if( $processes )
{
if( $processes.Count -eq 1 -and $processes[0].IndexOf(',') -ge 0 )
{
$processes = $processes -split ','
}
$params.Add( 'Name' , $processes )
}
if( $processIds )
{
if( $processIds.Count -eq 1 -and $processIds[0].IndexOf(',') -ge 0 )
{
$processIds = $processIds -split ','
}
$params.Add( 'Id' , $processIds )
}
if( $includeUsers -or $excludeUsers )
{
$params.Add( 'IncludeUserName' , $true ) ## Needs admin rights
if( $includeUsers.Count -eq 1 -and $includeUsers[0].IndexOf(',') -ge 0 )
{
$includeUsers = $includeUsers -split ','
}
if( $excludeUsers.Count -eq 1 -and $excludeUsers[0].IndexOf(',') -ge 0 )
{
$excludeUsers = $excludeUsers -split ','
}
}
if( $exclude -and $exclude.Count -eq 1 -and $exclude[0].IndexOf(',') -ge 0 )
{
$exclude = $exclude -split ','
}
if( $sessionIds -and $sessionIds.Count -eq 1 -and $sessionIds[0].IndexOf(',') -ge 0 )
{
$sessionIds = $sessionIds -split ','
}
if( $notSessionIds -and $notSessionIds.Count -eq 1 -and $notSessionIds[0].IndexOf(',') -ge 0 )
{
$notSessionIds = $notSessionIds -split ','
}
[int]$adjusted = 0
Get-Process @params -ErrorAction SilentlyContinue | ForEach-Object `
{
$process = $_
[bool]$doIt = $true
if( $excludeUsers -and $excludeUsers.Count -And $excludeUsers -contains $process.UserName )
{
Write-Verbose ( "`tSkipping {0} pid {1} for user {2} as specifically excluded" -f $process.Name , $process.Id , $process.UserName )
$doIt = $false
}
elseif( $doIt -and $includeUsers -and $includeUsers.Count -And $includeUsers -notcontains $process.UserName )
{
Write-Verbose ( "`tSkipping {0} pid {1} for user {2} as not included" -f $process.Name , $process.Id , $process.UserName )
$doIt = $false
}
elseif( $doIt -and $exclude -and $exclude.Count -And $exclude -contains $process.Name )
{
Write-Verbose ( "`tSkipping {0} pid {1} as specifically excluded" -f $process.Name , $process.Id )
$doIt = $false
}
elseif( $doIt -and $thisSession -And $process.SessionId -ne $thisSessionId )
{
Write-Verbose ( "`tSkipping {0} pid {1} as session {2} not {3}" -f $process.Name , $process.Id , $process.SessionId , $thisSessionId )
$doIt = $false
}
elseif( $doIt -and $process.Id -eq $activePid -And $idle -eq 0 ) ## if idle then we'll trim anyway as not being used (will have quit already if not idle if idle parameter specified)
{
Write-Verbose ( "`tSkipping {0} pid {1} as it is the foreground window process" -f $process.Name , $process.Id )
$doIt = $false
}
elseif( $doIt -and $sessionIds -and $sessionIds.Count -gt 0 -And $sessionIds -notcontains $process.SessionId.ToString() )
{
Write-Verbose ( "`tSkipping {0} pid {1} as session {2} not in list" -f $process.Name , $process.Id , $process.SessionId )
$doIt = $false
}
elseif( $notsessionIds -and $notSessionIds.Count -gt 0 -And $notSessionIds -contains $process.SessionId.ToString() )
{
Write-Verbose ( "`tSkipping {0} pid {1} as session {2} is specifically excluded" -f $process.Name , $process.Id , $process.SessionId )
$doIt = $false
}
elseif( $doIt -and $sessionsToTarget.Count -gt 0 -And $sessionsToTarget -notcontains $process.SessionId )
{
Write-Verbose ( "`tSkipping {0} pid {1} as session {2} which is not disconnected" -f $process.Name , $process.Id , $process.SessionId )
$doIt = $false
}
elseif( $doIt -and $above -gt 0 -And $process.WS -le $above )
{
Write-Verbose ( "`tSkipping {0} pid {1} as working set only {2} MB" -f $process.Name , $process.Id , [Math]::Round( $process.WS / 1MB , 1 ) )
$doIt = $false
}
elseif( $doIt -and $newOnly -and $process.StartTime -lt $monitoringStartTime )
{
Write-Verbose ( "`tSkipping {0} pid {1} as start time {2} prior to {3}" -f $process.Name , $process.Id , $process.StartTime , $monitoringStartTime )
$doit = $false
}
if( $doIt )
{
$action = "Process {0} pid {1} session {2} working set {3} MB" -f $process.Name , $process.Id , $process.SessionId , [Math]::Floor( $process.WS / 1MB )
if( $process.Handle )
{
if( $report )
{
[int]$thisMinimumWorkingSet = -1
[int]$thisMaximumWorkingSet = -1
[int]$thisFlags = -1 ## Grammar alert! :-)
## https://msdn.microsoft.com/en-us/library/windows/desktop/ms683227(v=vs.85).aspx
[bool]$result = [PInvoke.Win32.Memory]::GetProcessWorkingSetSizeEx( $process.Handle, [ref]$thisMinimumWorkingSet,[ref]$thisMaximumWorkingSet,[ref]$thisFlags);$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
if( $result )
{
## convert flags value - if not hard then will be soft so no point reporting that separately IMHO
[bool]$hardMinimumWorkingSet = $thisFlags -band 1 ## QUOTA_LIMITS_HARDWS_MIN_ENABLE
[bool]$hardMaximumWorkingSet = $thisFlags -band 4 ## QUOTA_LIMITS_HARDWS_MAX_ENABLE
$null = $results.Add( ([pscustomobject][ordered]@{ 'Name' = $process.Name ; 'PID' = $process.Id ; 'Handle Count' = $process.HandleCount ; 'Start Time' = $process.StartTime ;
'Hard Minimum Working Set Limit' = $hardMinimumWorkingSet ; 'Hard Maximum Working Set Limit' = $hardMaximumWorkingSet ;
'Working Set (MB)' = $process.WorkingSet64 / 1MB ;'Peak Working Set (MB)' = $process.PeakWorkingSet64 / 1MB ;
'Commit Size (MB)' = $process.PagedMemorySize / 1MB;
'Paged Pool Memory Size (KB)' = $process.PagedSystemMemorySize64 / 1KB ; 'Non-paged Pool Memory Size (KB)' = $process.NonpagedSystemMemorySize64 / 1KB ;
'Minimum Working Set (KB)' = $thisMinimumWorkingSet / 1KB ; 'Maximum Working Set (KB)' = $thisMaximumWorkingSet / 1KB
'Hard Minimum Working Set' = $hardMinimumWorkingSet ; 'Hard Maximum Working Set' = $hardMaximumWorkingSet
'Virtual Memory Size (GB)' = $process.VirtualMemorySize64 / 1GB; 'Peak Virtual Memory Size (GB)' = $process.PeakVirtualMemorySize64 / 1GB; }) )
}
else
{
Write-Warning ( "Failed to get working set info for {0} pid {1} - {2} " -f $process.Name , $process.Id , $LastError)
}
}
elseif( $pscmdlet.ShouldProcess( $action , 'Trim' ) ) ## Handle may be null if we don't have sufficient privileges to that process
{
## see https://msdn.microsoft.com/en-us/library/windows/desktop/ms686237(v=vs.85).aspx
[bool]$result = [PInvoke.Win32.Memory]::SetProcessWorkingSetSizeEx( $process.Handle,$minWorkingSet,$maxWorkingSet,$flags);$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
$adjusted++
if( ! $result )
{
Write-Warning ( "Failed to trim {0} pid {1} - {2} " -f $process.Name , $process.Id , $LastError)
}
elseif( $savings )
{
$now = Get-Process -Id $process.Id -ErrorAction SilentlyContinue
if( $now )
{
$saved += $process.WS - $now.WS
$trimmed++
}
}
}
}
else
{
Write-Warning ( "No handle on process {0} pid {1} working set {2} MB so cannot access working set" -f $process.Name , $process.Id , [Math]::Floor( $process.WS / 1MB ) )
}
}
}
if( $report )
{
if( [string]::IsNullOrEmpty( $outputFile ) )
{
if( -Not $nogridview )
{
$selected = $results | Sort-Object Name | Out-GridView -PassThru -Title "Memory information from $($results.Count) processes at $(Get-Date -Format U)"
if( $selected )
{
$selected | clip.exe
}
}
else
{
$results
}
}
else