14 октября 2015 г.

PowerShell Автоматическое снятие скриншотов


Задача (корпоративный сектор):
Делать снимки экрана раз в 10 сек в режиме Stealth, и складывать в папку на удаленном ПК
Казалось бы, ничего сложного, найти программу и сказать ей куда скидывать.

Но я не нашел подходящей программы, например ScreenMaster нужно настраивать под каждым пользователем, а с гостем вообще сложности. LiteManager имеет функцию съемки с экрана, но он запихивает картинки в свой архив. У всех вариантов еще есть минусы, кто платный, а кто не дружит с антивирусами.
Пошел к Гуглу, он меня перекидывает на статью Vadims Podāns, как делать скриншоты при помощи PowerShell, во, это то что нужно :-)
Александр Aka Kazun добавил в метод выше отображение курсора

Как обычно страдаю гигантизмом, и вот детище (в двух местах подсветка синтаксиса определяет экранировании кавычек, поэтому стоит стоит по паре кавычек):


#screen.ps1
#path - путь сохранения настроек
#Interval - в секундах между снимками
#ScreenFormat - расширение файла (jpeg,png,gif)
#File [булево] - загрузка настроек из файла
Param (
[string]$Path,
[string]$Interval,
[string]$ScreenFormat,
[switch]$File
)
#Переменные
$prg=${env:ProgramFiles};
$install_path=$prg+""\ScreenShoter\""; #$install_path=$prg+"\ScreenShoter\"
$IniFileName="screen.ini";
$LogFile = $install_path+"\screen.log";
$pcName=${env:computername};
#При каждом запуске удаляем лог файл
if (Test-Path $LogFile) {
 Remove-Item $LogFile -Force | Out-Null
 New-Item -ItemType file $LogFile -Force | Out-Null
}
#Простая функция логирования текст на монитор и в файл
function Logi ($text) {
 Write-Host $text;
 if (Test-Path $LogFile) {
  Write-output $text | out-file "$LogFile" -APPEND
 }
}
#Паузе перед сбросом скрипта
function SleepBreak {
 Logi ("Завершение программы")
 Start-Sleep -Seconds 20
 break
}
#Если $File истина загрузить настройки из файла, иначе загрузить из параметров, иначе установить по умолчанию.
#Если файл не найден сброс
if ($File -eq $true) {
 if (Test-Path ($install_path+$IniFileName)) {
  Logi ("")
  Logi ("Чтение файла настроек...")
  $SetupIni=Get-Content ($install_path+$IniFileName);
  for ($i=0; $i -lt $SetupIni.count; $i++ ){
   if ($SetupIni[$i] -like "Path=*") {
    $Path=$SetupIni[$i].Replace("Path=","");
    Logi (" Место сохранения скриншотов: "+$Path);
   }elseif ($SetupIni[$i] -like "Interval=*") {
    $Interval=$SetupIni[$i].Replace("Interval=","");
    Logi (" Интервал съемки: "+$Interval+" сек.");
   }elseif ($SetupIni[$i] -like "ScreenFormat=*") {
    $ScreenFormat=$SetupIni[$i].Replace("ScreenFormat=","");
    Logi (" Формат файла: "+$ScreenFormat);
   }
  }
 }else{
  Logi ($IniFileName+" не найден, работа прервана");
  SleepBreak
 }
}else{
 if ($Path -eq ""){
  $Path=$env:HOMEDRIVE;
  Logi("Программе не передан путь сохранения, будет использован стандартный: "+$Path)
 }

 if ($Interval -eq ""){
  $Interval=10;
  Logi("Программе не передан интервал сохранения, будет использован стандартный: "+$Interval)
 }

 if ($ScreenFormat -eq ""){
  $ScreenFormat="png";
  Logi("Программе не передан формат сохранения, будет использован стандартный: "+$ScreenFormat)
 }
}


[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
function screenshot{
 #Имя файла обновляется каждый вызов функции
 $date_time = get-date -uformat "%Y.%m.%d_%H.%M%S";
 $FullFileName=$Path+""\""+$pcName+"_"+$date_time+"."+$ScreenFormat; #"\"
 #Получаем размеры экрана
 $size = [Windows.Forms.SystemInformation]::VirtualScreen
 #Создаем объект, куда поместим картинку
 $bitmap = new-object Drawing.Bitmap $size.width, $size.height
 #Создаем объект Графики с привязкой к BitMap
 $graphics = [Drawing.Graphics]::FromImage($bitmap)
 #Попытка скопировать побитно экран в объект Графики
 try {
  $graphics.CopyFromScreen($size.location,[Drawing.Point]::Empty,$size.size)
  $bitmap.Save($FullFileName,[System.Drawing.Imaging.ImageFormat]::$ScreenFormat)
 }
 catch [System.Management.Automation.MethodInvocationException]{
  $except=$_;
  if (($except.InvocationInfo.Line).Replace("`t","") -eq '$bitmap.Save($FullFileName)'){
   if ($except.Exception.InnerException -like "*В GDI+ возникла ошибка общего вида.*") {
    Logi ($date_time+" Ошибка записи")
   } else {
    Logi ($date_time+" возникла другая ошибка при записи")
    Logi ($except.Exception.InnerException)
   }
  } elseif (($except.InvocationInfo.Line).Replace("`t","") -eq '$graphics.CopyFromScreen($size.location,[Drawing.Point]::Empty,$size.size)'){
   if ($except.Exception.InnerException -like "*Неверный дескриптор*") {
    Logi ($date_time+" сеанс неактивен")
   } else {
    Logi ($date_time+" непредвиденная ошибка снятия скриншота")
    Logi ($except.Exception.InnerException)
   }
  } else {
   Logi ("Возникла другая ошибка в строке: "+$except.InvocationInfo.Line)
  }
 } catch {
  Logi ($date_time+" возникла незарегистрированная ошибка")
  Logi ($_.Exception.InnerException)
 } finally {
  #Зачистка памяти
  $graphics.Dispose()
  $bitmap.Dispose()
  [gc]::collect()
 }
}
#Получение скриншота при первом запуске программы
screenshot
#Бесконечный цикл
do{
 Start-Sleep -s $Interval.REplace("""","")
 screenshot
}while(0 -eq 0)


Скрипт выполняет создание скриншота с параметрами:

  1. Путь
  2. Интервал
  3. Формат файла (качество никак не регулируется)
Параметры можно передавать в файле screen.ini:

Path=\\192.168.9.15\store\SCREEN\buhg
Interval=10
ScreenFormat=jpg

В скрипте используется блок try/catch/finally с условием, этот блок обрабатывает следующие исключения:

Исключение при вызове "CopyFromScreen" с "3" аргументами: "Неверный дескриптор"
C:\SCREENSHOTER\screen.ps1:85 знак:27
+         $graphics.CopyFromScreen <<<< ($size.location,[Drawing.Point]::Empty, $size.size)
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException
И
Исключение при вызове "Save" с "1" аргументами: "В GDI+ возникла ошибка общего вида."
C:\SCREENSHOTER\screen.ps1:107 знак:15
+         $bitmap.Save <<<< ($FullFileName)
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

Имена файлов состоят из имени ПК и даты до миллисекунд.

Этот скрипт можно скомпилировать в exe при помощи PowerGUI, не рекомендуется использование PS2EXE, так как скомпилированный этим компилятором ехе не может обрабатывать исключения, нет вывода.
В PowerGUI я создал два exe - один screen.exe (без консоли), второй screen_debug.exe (с консолью, для чтения вывода перед вводом в эксплуатацию).
Скомпилированный файл не требует настройки:

#Разрешить выполнение не подписанных скриптов
powershell.exe Set-ExecutionPolicy RemoteSigned 
#Значение по-умолчанию
powershell.exe Set-ExecutionPolicy Restricted

Далее прописав screen.exe в:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
Мы получим скрытую съемку (в диспетчере задач будет висеть минимум 1 процесс screen.exe).
Если заблокировать ПК, "программа" будет работать дальше, но без снимков ( в неактивном сеансе файл на выходе - черный лист).
Если сменить пользователя (без выхода), то у нового запустится новый экземпляр "программы".

Под мои задачи программа идеальна....
Вот только одна проблема, установка выглядит так:
  1. Создать папку в "Program Files"
  2. Скопировать туда файлы
  3. Создать запись в реестре
  4. Запустить screen.exe -Arguments -File
Один раз ладно, а вот раз 20-40-60 как-то напряжно, поэтому я решил сделать инсталляционный файл :-)

#setup.ps1
$prg=${env:ProgramFiles}
$install_path=$prg+""\ScreenShoter\"" #$install_path=$prg+"\ScreenShoter\"
$FileName="screen.exe"
$FullFileName="`""+$install_path+$FileName+"`"";
$IniFileName="screen.ini"
$curDir = $MyInvocation.MyCommand.Definition | split-path -parent
$LogFile = $curDir+"\setup.log";
$RegPath="Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\"" #Дополнительная кавычка
$RegName="ScreenShoter"
$FullRegPath=$RegPath+$RegName;
Set-Location $curDir
$InstallOrUninstall = Read-Host "Установить программу [y (установить)/ n (удалить)]";
if (Test-Path $LogFile) {
 Remove-Item $LogFile -Force | Out-Null
}
function Logi ($text) {
 Write-Host $text;
 Write-output $text | out-file "$LogFile" -APPEND

}
function SleepBreak {
 Logi ("Завершение программы")
 Start-Sleep -Seconds 20
 break
}
function KillProc ($NameProc){
 $NameProc=$NameProc.Replace(".exe","")
 try{
  $Process=Get-Process $NameProc -ErrorAction Stop
 } catch {
  Logi ($NameProc+" не запущен") 
 }
 if ($Process){
  If ( $Process.Path -eq ($install_path+$NameProc) ){
   Logi ($NameProc+" процесс найден")
   try{
    Stop-Process $Process.id -ErrorAction Stop
   } catch {
    Logi ($NameProc+" не удалось остановить")
    Logi (" Завершите процесс вручную")
    SleepBreak
   }
  }else{
   Logi ($NameProc+" путь к файлу не соответствует, завершите процесс вручную") 
   SleepBreak
  }
 }

}
function UnInstall{
 #Удаляем папку
 KillProc ($FileName)
 KillProc ($FileName.Replace(".exe","_debug.exe"))

 if (Test-Path $install_path){
  try{
   Remove-Item $install_path -Recurse -Force -ErrorAction Stop
  }
  catch {
   Logi (" Не удалось удалить файлы программы")
   Logi (" Run As Administrator!")
   SleepBreak
  }
  Logi (" Файлы программы удалены")
 }else{
  Logi (" Папки "+$install_path+" не существует!")
 }
 #Удаляем запись в реестре
 $CheckReg=Get-ItemProperty -Path $RegPath -Name $RegName | Out-Null
 If (!$CheckReg){
  Logi (" Записи в реестре не существует!!!")
 }else{
  try{
   Remove-ItemProperty -Path $RegPath -Name $RegName -Force -ErrorAction Stop
  }
  catch {
   Logi (" Не удалось удалить запись в реестре")
   Logi (" Run As Administrator!")
   SleepBreak
  }
  Logi (" Запись реестра удалена")
 }
 Logi ("Деинсталляция завершена")
 SleepBreak
}

if ($InstallOrUninstall -eq "n"){
 Logi ("Деинсталляция продукта")
 UnInstall
 SleepBreak
}else{
 $InstallDebug = Read-Host "Debug [y/n]";
}
if ($InstallDebug -eq "y"){
 $install_path=$prg+""\ScreenShoter\"" # $install_path=$prg+"\ScreenShoter\"
 $FileName="screen_debug.exe"
 $FullFileName="`""+$install_path+$FileName+"`"";
}
function CreatePath{
 try{
  New-Item -Path $install_path -Type directory -Force -ErrorAction Stop | Out-Null
 }
 catch {
  Logi (" Папка не создана!")
  Logi (" Run As Administrator!")
  SleepBreak
 }
}

function CopyInPath{
 param([string]$file)
 try{
  Copy-Item $file $install_path -Force -ErrorAction Stop
 }
 catch {
  Logi (" Файлы программы не скопированны!")
  Logi (" Run As Administrator")
  SleepBreak
 }
}
function CreateRegParam{
 try{
  New-ItemProperty -Path $RegPath -Name $RegName -PropertyType String -Value ($FullFileName+" -Arguments -File") -Force -ErrorAction Stop | Out-Null
 }
 catch {
  Logi (" Не удалось выполнить добавление записи в реестр")
  Logi (" Run As Administrator")
  SleepBreak
 } 

}
if (Test-Path $install_path) {
 Logi (" Директория существует.")
} else {
 CreatePath
}

CopyInPath -file $FileName
CopyInPath -file ($FileName+".config")
CopyInPath -file ($IniFileName)

if (Test-Path $IniFileName) {
 Logi ("")
 Logi ("Чтение файла настроек...")
 $SetupIni=Get-Content $IniFileName;
 for ($i=0; $i -lt $SetupIni.count; $i++ ){
  if ($SetupIni[$i] -like "Path=*") {
   $PathFromScreen=$SetupIni[$i].Replace("Path=","");
   Logi (" Место сохранения скриншотов: "+$PathFromScreen);
  }elseif ($SetupIni[$i] -like "Interval=*") {
   $IntervalFromScreen=$SetupIni[$i].Replace("Interval=","");
   Logi (" Интервал съемки: "+$IntervalFromScreen+" сек.");
  }elseif ($SetupIni[$i] -like "ScreenFormat=*") {
   $ScreenFormatFromScreen=$SetupIni[$i].Replace("ScreenFormat=","");
   Logi (" Формат файла: "+$ScreenFormatFromScreen);
  }
 }
}else{
 Logi ($IniFileName+" не найден, установка прервана");
 SleepBreak
}

Logi ("")
Logi ("Добавление записи в реестр")
CreateRegParam
Logi (" Запись добавлена")
Logi ("")
logi ("Запуск программы")
 #$NetParams = @("-Arguments -Path `"$PathFromScreen`" -Interval `"$IntervalFromScreen`" -ScreenFormat `"$ScreenFormatFromScreen`"")
 $NetParams = @("-Arguments -File")
 Start-Process ($install_path+$FileName) -ArgumentList $NetParams
Logi ("Завершение установки...")
SleepBreak

Setup.ps1 не только устанавливает, но и удаляет программу, о чем скрипт спрашивает в самом начале. Также, при установке, скрипт спрашивает какую версию ставить debug или нет, при первом запуске лучше установить debug, чтобы увидеть ошибки.
Попытка скомпилировать файл setup.ps1 выявила еще одну проблему. Скомпилированный файл при запуске "распаковывается" $temp, каждый раз новая папка, поэтому выполнить установку (часть которой, копирование файлов из папки запуска) exe не может.
Но, я решил использовать другой способ, создаем батник:

#Разрешить выполнение не подписанных скриптов
powershell.exe Set-ExecutionPolicy RemoteSigned 
start /wait powershell.exe ./setup.ps1
#Значение по-умолчанию
powershell.exe Set-ExecutionPolicy Restricted

И все идет как по маслу )
Для использования скрипта и exe требуется установленный PowerShell не ниже 2, .NET FrameWork не ниже 4.х
Скачать и посмотреть можно по ссылке
Что мне не понравилось в окончательной реализации:

  1. Вес exe в разы выше оригинала (оригинал - 9Кб, exe - 191Кб)
  2. Потребление памяти варьируется от 17Мб до 38Мб (мне кажется многовато)


Всё описанное предоставляется в ознакомительных целях. Все действия описанные в мануалах Вы выполняете на свой страх и риск.
Напоминаю: сбор информации с ПК, без согласия владельца, противозаконно!!!

План на далекое будущее:
  1. Сжатие изображений
  2. Определение простоя ПК
  3. Расчесать лапшу
Up 15/10/2015
Как я уже писал ранее, качество практически не регулируется. Только форматом, а именно для JPEG - 24bit глубина цвета, для PNG - 32bit.
На классической теме это практически незаметно, но на Aero и стандартных обоях, PNG весит в 10 раз больше

Комментариев нет:

Отправка комментария