PowerShell командлет Copy-Item используется для копирования файлов между локальными, сетевыми каталогами или между компьютерами по сети через WinRM. Командлет Copy-Item предоставляет большое количество опций, которые можно использовать в разных сценариях копирования файлов и каталогов (по своим возможностям этот командлет почти не уступает утилите robocopy). Например:
- перезапись файлов (override)
- фильтрация по имени/шаблону
- исключение по имени/шаблону
- Verbose режим
- Копирование файлов с/на удаленные компьютеры
Начнем с простых примеров использования Copy-Item и будем переходить к более сложным.
Копирование файлов и каталогов
Чтобы скопировать один файл 1.txt из каталога C:\SourceFolder\ в F:\DestFolder\, выполните:
Copy-Item -Path "C:\SourceFolder\1.txt" -Destination "F:\DestFolder\1.txt"
Можно использовать сокращенный синтаксис командлета, пропустив указание параметров Path и Destination:
cpi "C:\SourceFolder\1.txt" "F:\DestFolder\1.txt"
Теперь скопируем каталог C:\SourceFolder\folder в F:\DestFolder\folder. В папке folder находится файл 1.txt. Обратите внимание что без ключа –Recurse, папка folder копируется без содержимого:
Copy-Item -Path "C:\SourceFolder\folder" -Destination "F:\DestFolder\folder" -Recurse
С помощью Copy-Item также можно просто объединить файлы из несколько директорий в одну (слияние директории), для этого нужно перечислить директории в ключе –Path:
Copy-Item -Path "C:\SourceFolder\*", "C:\SourceFolder2\*", "C:\SourceFolder3\*" -Destination "F:\DestFolder\"
Копирование с заменой и копирование с заменой read-only файлов
Copy-Item по умолчанию при копировании заменяет файлы в целевом каталоге. Никаких дополнительных параметров указывать не нужно. При копировании каталога, если нужно заменить каталог в целевой папке, нужно использовать ключ –Force, иначе будет ошибка “Элемент folder с указанным именем уже существует — DirectoryExists”.

Для перезаписи файла с атрибутом read-only, нужно использовать ключ -Force. Если его не использовать, вы получите ошибку “отказано в доступе по пути… CopyFileInfoItemUnauthorizedAccessError”.

Чтобы скопировать файл с перезаписью файла с read-only атрибутом используйте параметр Force.
Copy-Item -Path "C:\SourceFolder\1.txt" -Destination "F:\DestFolder\1.txt" -Force
Чтобы Copy-Item скопировал файлы из одной папки в другую без замены существующих файлов, можно использовать этот простой скрипт
Copy-Item (Join-Path "C:\SourceFolder\" "*") "F:\DestFolder\" -Exclude (Get-ChildItem "F:\DestFolder\") -Recurse
Этот скрипт скопирует все файлы и папки из C:\SourceFolder в F:\DestFolder без замены файлов уже существующих в F:\DestFolder
Копирование с фильтрацией по шаблону
С помощью Copy-Item можно скопировать файлы/директории выбранные с помощью wildcard символа * или с помощью символа ?. Также поддерживаются некоторые регулярные выражения
- * — обозначает любое количество любых символов
- ? – обозначает 1 любой символ
- [a-z], [0-9] – символы между a-z и цифры между 0 и 9
Для примера возьмём такую структуру файлов:

Выполним копирование командой:
Copy-Item -Path "C:\SourceFolder\fol*" -Destination "F:\DestFolder\"
Результат в F:\DestFolder\

Теперь чистим папку назначения и выполняем:
Copy-Item -Path "C:\SourceFolder\folder[0-3]" -Destination "F:\DestFolder\"
Результат:

Папка без цифры в окончании не скопировалась, потому что folder[0-3] подразумевает что после folder будет как минимум еще 1 символ между 0 и 3
Исключение файлов при копировании
С помощью ключа –Exclude можно исключить файлы при копировании. Например, следующай команда скопирует все файлы кроме файлов с расширением txt.
Copy-Item -Path "C:\SourceFolder\*" -Destination "F:\DestFolder\" -Recurse -Force -Exclude "*.txt"
Аналогичным же образом можно применить ключ –Include, например
Copy-Item -Path "C:\SourceFolder\*" -Destination "F:\DestFolder\" -Recurse -Force -Include "*.txt"
Скопирует только txt файлы. Хотя для простоты гораздо удобнее использовать при копировании вид
-Path "C:\SourceFolder\*.txt"
.
Копирование файлов на удаленный компьютер по сети
Copy-File может копировать не только по SMB протоколу, но и через WinRM (WSMan).
Создайте новую сессию с компьютером testnode1 и выполните копирование в её контексте:
$session = New-PSSession -ComputerName testnode1
Copy-Item -Path "C:\SourceFolder\*" -ToSession $session -Destination "C:\SourceFolder\" -Recurse -Force
Эта команда скопирует файлы с локального компьютера из директории C:\SourceFolder на компьютер testnode1 в C:\SourceFolder\.
Test-WSMan -ComputerName testnode1

Если WSMan не настроен, вы можете выполнить его быструю конфигурацию. Для этого откройте командную строку с правами администратора и выполните
winrm quickconfig

Также можно копировать и через обычные сетевые SMB шары, для этого просто используйте UNC формат сетевого пути.
Copy-Item -Path "C:\SourceFolder\*" -Destination "\\testnode1\C$\copy_tutorial\"
Можно скопировать файл с удаленного компьютера. Принцип такой же, как и при копировании файлов на удаленный компьютер, за исключением параметра –ToSession, вместо него нужно использовать –FromSession:
$session = New-PSSession -ComputerName testnode1
Copy-Item -FromSession $session -Path "C:\SourceFolder\*" -Destination "F:\DestFolder\" -Recurse -Force
Эта команда скопирует содержимое папки C:\SourceFolder\ с компьютера testnode1 на локальный компьютер в директорию F:\DestFolder
Ключ PassThru
Командлет Copy-Item (как и многие другие командлеты PowerShell) не возвращает результатов в консоль. Параметр PassThru применяется скриптах, или для лог-файлов, когда нужно получить список скопированных файлов и работать с ним дальше. Рассмотрим пример
$items = Copy-Item -Path "C:\SourceFolder\*" –Destination "\\testnode1\C$\copy_tutorial\" -PassThru
Переменная
$items
будет содержать список скопированных файлов, с которым вы можете работать дальше.Это значит что вы можете напрямую работать с этими файлами. Например выполнив команду
Remove-Item $items[0]
, вы удалите директорию folder.

Ключ Verbose
При использовании ключа -Verbose вы получите подробный лог операций копирования. Например, вывод команды
Copy-Item -Path "C:\SourceFolder\*.txt" -Destination "F:\DestFolder\" -Recurse -Force -Verbose

Несколько полезных скриптов с Copy-Item
Скопировать только файлы:
Get-ChildItem "C:\SourceFolder" -File -Recurse | Copy-Item -Destination "F:\DestFolder"
Скопировать структуру папок, без файлов:
$path = Get-ChildItem "C:\SourceFolder" -Recurse | ?{$_.PsIsContainer -eq $true}
$dest = "F:\DestFolder\"
$parent = $path[0].Parent.Name
$path | foreach {
$_.FullName -match "$parent.+"
New-Item -ItemType directory ($dest + $Matches[0])
}
Copy-Item очень простой и удобный в использовании командлет PowerShell для выполнения операций копирования и перемещения файлов. В сочетании с другими инструментами PowerShell, Copy-Item также является мощным инструментом для написания скриптов.


Здравствуйте. Столкнулся с непонятками:
gci $VSCPath | ? {$_.PSIsContainer -eq $true} | Copy-Item -Destination $dest -Recurseсодержимое первой по порядку папки высыпается напрямую в Destination, остальные копируются нормально, вместе с содержимым.
На другом хосте тоже самое.
gci $VSCPath | ? {$_.PSIsContainer -eq $true}djpdhfoftnПодскажите, как можно это вылечить?
Добрый день. Попробуйте пройтись по папкам через цикл for-each, передавая каждую папку как аргумет для copy-item. Убедитесь что пути директорий которые Вы передаёте корректные (подебажьте через write-host, например)
Так вот он выше список каталогов.
Дело не в пути. Дело в том что он первый.
При
gci $VSCPath |sort -Descending | ? {$_.PSIsContainer -eq $true}высыпается содержимое другой папки, которая до этого копировалась нормальноюНе могу Вам конкретно ответить в чём проблема, так как для этого нужно разбираться с дебагом Вашего скрипта. Возможно дело в том, что первая директория по какой-то причине не так передается в пайплайн. Попробуйте использовать get-childitem
То же самое. Первый по порядку высыпается в корень.
$Files = gci $VSCPath | ? {$_.PSIsContainer -eq $true}|Select -Property Name,@{label=”Path”;Expression ={$_.FullName}} | Select -Property Path # | Copy-Item -Destination $dest -Recurse
foreach ($sFile in $Files)
{
#Copy-Item -LiteralPath $sFile.Path -Destination $dest -Recurse
$sFile.Path +" -- "+ $dest
}
Я так понял, что Copy-Item не создаёт директорию, если её нет.
Copy-Item -LiteralPath «C:\testPS\1\222.jpg» -Destination «C:\testPS\2\222.jpg»
(ошибка)
«Не удалось найти часть пути «C:\testPS\2\222.jpg».»
Приходится директорию отделять от файла каждый раз в цикле.
Печально, что «Copy-item» не умеет копировать права NTFS папок и файлов , а также переподключаться при обрывах в сети.
Спасибо за приведенные примеры
Был бы интересен вариант копирования с одной сетевой шары в другую а после окончания операции проверить на соответствие и в случае если в источнике добавились файлы или изменились то провести копирование только этих объектов .
Добрый день. Помогите с поиском решения для переноса файлов.
Есть csv файл в который выгружен список файлов, которые не изменялись в течении 2 лет, в файле порядка 3000000 строк. Задача перенести на новый диск все файлы кроме .xl*.
Написал вот такой скрипт, который еще попутно ведет лог того что перенесено.
$Paths = Import-Csv -Path ‘C:\temp\OldFiles.csv’ -Delimiter ‘;’
$Paths = $Paths.FullName
$Copylog = «C:\temp\copylog_all.csv»
foreach ($Path in $Paths){
foreach ( $Newpath in $Path){
If ($Path.Substring(0,2) -eq «\\») {$Newpath = («\\?\Y:» + $Path.Remove(0,12))}
$Destpath = Split-Path $Newpath -Parent
$Item = Move-Item -Path $Path -Destination $Destpath -Exclude *.xl* -PassThru -Verbose
$Item |Select-Object fullname | Export-Csv -Force -Path $Copylog -Encoding UTF8 -Delimiter «;»
-NoTypeInformation -Append
}
}
путь в файле примерно такого вида
«\\?\D:\_Dept\111111\2222\33333\44444\55555\6666\777777\888888.pdf»
Структура папок на новом диске полностью перенесена со всеми группами доступа.
Проблема в том что PowerShell обрабатывает этот скрипт очень медленно, за 2 недели прошел порядка 1кк строк.
Есть ли способ его ускорить? Или какие-нибудь решения с помощью Robocopy?
Что означает знак «+» в последнем примере?
$_.FullName -match «$parent.+»
Добрый день!
Как копировать когда я удалённо уже подключился через Enter-PSSession к себе на локальный комп ?
Вопрос.
Когда объявляю переменную она сразу отрабатывает.
Т.е.
$copy = Copy-Item "$folders_on" -Destination "\\$computer\$folders_in" -Recurse -Forceи начинается копирование файлов.
Хотя копирование должно начаться, когда я напишу её после, т.е.
$copyПодскажете, как сделать так, чтобы копирование не начиналось при объявлении переменной, а только после того, как она выла вызвана.
Copy-Item "$folders_on" -Destination "\\$computer\$folders_on" -Recurse -ForceВопрос:
Т.к. есть ключ
-Force, то должна быть перезапись файлов.Но если на ПК назначения уже есть папка $folders_on, то всё будет копироваться в подпапку $folders_on.
Т.е. если конечная папка например называется 123, то при её наличии в пути назначения, всё будет копироваться в 123\123,
т.е. если на ПК источнике есть папка
С:\123и на ПК назначения есть папкаC:\123, ($folders_on = "C:\123"), то при запуске скрипта всё копируется на ПК назначения в C:\123\123.Как сделать так, чтобы копировалось в C:\123 c перезаписью? Не в подпапку C:\123\123.
Как сделать так, чтобы папка $folders_on перезаписывалась и не было копирования в подпапку $folders_on?
UPD
Ошибся в предыдущем коде:
Copy-Item «$folders_on» -Destination «\\$computer\$folders_in» -Recurse -Force
где
$folders_on = 'C:\123'$folders_in = 'C$\123Вопрос 2.
А если копировать так (со звёздочкой):
Copy-Item "С:\123\*" -Destination "\\$computer\C$\123" -Recurse -Force, то на ПК назначения создаётся файл в папке C:\123 c именем 123 без расширения, хотя это должна быть папка с фалами. В чём может быть ошибка?Народ, а есть способ скопировать файл с помощью powershell на пару десятков машин в сети разом? либо перечислив их через запятую или указав файл от куда брать имена машин?
<#
.SYNOPSIS
Синхронизация выписок: проверка рабочего дня, порога файлов, копирование на сетевой шар и архивирование локально с подробным логированием и уведомлениями по SMTP.
.DESCRIPTION
Скрипт предназначен для запуска в Task Scheduler на Windows Server 2016 (PowerShell 5.1).
Работает по следующему алгоритму:
1. Блокировка (предотвращение параллельных запусков).
2. Создание каталога логов и файла лога для текущего запуска.
3. Проверка — сегодня рабочий день (Пн–Пт). Если нет — выход.
4. Подсчёт файлов в c:\statement\file (только корень, не рекурсивно). Исключаем c:\statement\arc.
5. Если файлов
#region ========== Настройки (отредактируйте перед запуском) ==========
# Пути
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$LogsRoot = Join-Path -Path $ScriptPath -ChildPath ‘logs’ # c:\script\logs (по умолчанию)
$SourceDir = ‘C:\statement\file’ # исходная папка с файлами (корень)
$ArchiveRoot = ‘C:\statement\arc’ # корень архива; внутри создаётся yyyy-MM-dd
# Сетевой ресурс назначения
$DestShare = ‘\\rs144\statement’ # сетевой шар назначения
# Порог файлов
$MinFilesThreshold = 40 # продолжать только если count > 40
# Копирование/повторы
$MaxAttempts = 3 # число попыток для копирования каждого файла
$RetryDelaySeconds = 30 # пауза между попытками
# Блокировка параллельных запусков (название Mutex)
$MutexName = ‘Global\SyncStatementsMutex_Predator89’ # можно изменить, если нужно
# SMTP (заполните перед использованием)
$SmtpHost = ‘smtp.example.local’ # <— УКАЖИТЕ SMTP-сервер
$SmtpPort = 25 # <— УКАЖИТЕ порт
$MailFrom = 'noreply@example.local' # <— УКАЖИТЕ From
$MailTo = 'ops@example.local' # ‘$destFull’ (попытка $attempt из $MaxAttempts).» ‘DEBUG’
# Copy-Item с -Force перезапишет, но по ТЗ конфликтов не ожидается
Copy-Item -Path $srcFull -Destination $destFull -Force -ErrorAction Stop
# Для надёжности можно проверить существование файла на шаре и размер:
if (Test-Path -Path $destFull) {
$srcSize = (Get-Item -LiteralPath $srcFull -ErrorAction Stop).Length
$dstSize = (Get-Item -LiteralPath $destFull -ErrorAction Stop).Length
if ($srcSize -eq $dstSize) {
$copied = $true
Write-Log «Файл успешно скопирован и проверен (размеры совпадают): $f.Name» ‘INFO’
} else {
Write-Log «Предупреждение: размеры не совпадают (src=$srcSize, dst=$dstSize). Попытка повторить.» ‘WARN’
Start-Sleep -Seconds $RetryDelaySeconds
}
} else {
Write-Log «Файл не найден на шаре после Copy-Item. Попытка повторить.» ‘WARN’
Start-Sleep -Seconds $RetryDelaySeconds
}
} catch {
Write-Log «Ошибка при копировании файла ‘$srcFull’: $_» ‘ERROR’
if ($attempt -lt $MaxAttempts) {
Write-Log «Ожидание $RetryDelaySeconds сек. и повтор.» ‘DEBUG’
Start-Sleep -Seconds $RetryDelaySeconds
}
}
} # end while
if (-not $copied) {
Write-Log «НЕ удалось скопировать файл после $MaxAttempts попыток: $srcFull» ‘ERROR’
$failedFiles += $f.Name
# Продолжаем попытки копирования остальных файлов (можно прервать сразу, но мы собираем все ошибки)
}
} # end foreach
# 5) Обработка результатов копирования
if ($failedFiles.Count -gt 0) {
$subject = «Ошибка копирования»
$body = «При копировании файлов возникли ошибки.`nНеисправные файлы:`n» + ($failedFiles -join «`n»)
Write-Log «Копирование завершилось с ошибками. Не перемещаем исходники. Формируем уведомление.» ‘ERROR’
Send-Notification -Subject $subject -Body $body
exit 5
}
Write-Log «Все файлы успешно скопированы на $DestShare.» ‘INFO’
# 6) Архивирование (перемещение исходников)
# Формируем папку архива формата yyyy-MM-dd
$archiveDateFolder = (Get-Date).ToString(‘yyyy-MM-dd’)
$archiveDir = Join-Path -Path $ArchiveRoot -ChildPath $archiveDateFolder
if (-not (Test-Path -Path $archiveDir)) {
Write-Log «Создаём архивную папку: $archiveDir» ‘DEBUG’
try {
New-Item -Path $archiveDir -ItemType Directory -Force | Out-Null
} catch {
Write-Log «ОШИБКА: не удалось создать папку архива $archiveDir: $_» ‘ERROR’
Send-Notification -Subject «Ошибка архивации» -Body «Не удалось создать папку архива $archiveDir. Ошибка: $_»
exit 6
}
}
# Перемещаем файлы (Move-Item)
$movedCount = 0
$moveErrors = @()
foreach ($f in $files) {
$srcFull = $f.FullName
$destFull = Join-Path -Path $archiveDir -ChildPath $f.Name
try {
Write-Log «Перемещение файла ‘$srcFull’ -> ‘$destFull’.» ‘DEBUG’
Move-Item -Path $srcFull -Destination $destFull -Force -ErrorAction Stop
$movedCount++
} catch {
Write-Log «ОШИБКА при перемещении файла ‘$srcFull’ в архив: $_» ‘ERROR’
$moveErrors += $f.Name
}
}
if ($moveErrors.Count -gt 0) {
Write-Log «Некоторые файлы не удалось переместить в архив: $($moveErrors -join ‘, ‘)» ‘ERROR’
# Несмотря на проблему с перемещением, отправим уведомление об ошибке архивации
$subject = «Ошибка архивации»
$body = «Файлы были успешно скопированы на $DestShare, но при перемещении в архив возникли ошибки.`nНе перемещены:`n» + ($moveErrors -join «`n»)
Send-Notification -Subject $subject -Body $body
# В зависимости от требований можно попытаться откатить, но по ТЗ просто уведомляем и выходим
exit 7
}
Write-Log «Успешно перемещено файлов в архив: $movedCount. Архив: $archiveDir» ‘INFO’
# 7) Отправка успешного уведомления
$subject = «Выписки»
$body = «Успешно скопировано $fileCount файлов в $DestShare.`nФайлы перемещены в архив: $archiveDir»
Send-Notification -Subject $subject -Body $body
Write-Log «Операция выполнена успешно. Завершение.» ‘INFO’
exit 0
} catch {
# Ловим неожиданные исключения
Write-Log «НЕОЖИДАННАЯ ОШИБКА: $_» ‘ERROR’
try {
Send-Notification -Subject «Ошибка синхронизации (исключение)» -Body «При выполнении скрипта возникло исключение: `n$_»
} catch {
Write-Log «Не удалось отправить уведомление об исключении: $_» ‘ERROR’
}
exit 99
} finally {
# Очистка: освобождение Mutex (если не было раньше)
try {
if ($mutexAcquired -and $global:mutex -ne $null) {
$global:mutex.ReleaseMutex() | Out-Null
$global:mutex.Dispose()
Write-Log «Mutex освобождён (в finally).» ‘DEBUG’
}
} catch {
Write-Log «Ошибка при освобождении Mutex в finally: $_» ‘ERROR’
}
Write-Log «=== Конец выполнения скрипта ===» ‘INFO’
}
<#.SYNOPSIS
Синхронизация выписок: проверка рабочего дня, порога файлов, копирование на сетевой шар и архивирование локально с подробным логированием и уведомлениями по SMTP.
.DESCRIPTION
Скрипт предназначен для запуска в Task Scheduler на Windows Server 2016 (PowerShell 5.1).
Работает по следующему алгоритму:
1. Блокировка (предотвращение параллельных запусков).
2. Создание каталога логов и файла лога для текущего запуска.
3. Проверка — сегодня рабочий день (Пн–Пт). Если нет — выход.
4. Подсчёт файлов в c:\statement\file (только корень, не рекурсивно). Исключаем c:\statement\arc.
5. Если файлов
#region ========== Настройки (отредактируйте перед запуском) ==========
# Пути
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$LogsRoot = Join-Path -Path $ScriptPath -ChildPath 'logs' # c:\script\logs (по умолчанию)
$SourceDir = 'C:\statement\file' # исходная папка с файлами (корень)
$ArchiveRoot = 'C:\statement\arc' # корень архива; внутри создаётся yyyy-MM-dd
# Сетевой ресурс назначения
$DestShare = '\\rs144\statement' # сетевой шар назначения
# Порог файлов
$MinFilesThreshold = 40 # продолжать только если count > 40
# Копирование/повторы
$MaxAttempts = 3 # число попыток для копирования каждого файла
$RetryDelaySeconds = 30 # пауза между попытками
# Блокировка параллельных запусков (название Mutex)
$MutexName = 'Global\SyncStatementsMutex_Predator89' # можно изменить, если нужно
# SMTP (заполните перед использованием)
$SmtpHost = 'smtp.example.local' # <-- УКАЖИТЕ SMTP-сервер
$SmtpPort = 25 # <-- УКАЖИТЕ порт
$MailFrom = 'noreply@example.local' # <-- УКАЖИТЕ From
$MailTo = 'ops@example.local' # '$destFull' (попытка $attempt из $MaxAttempts)." 'DEBUG'
# Copy-Item с -Force перезапишет, но по ТЗ конфликтов не ожидается
Copy-Item -Path $srcFull -Destination $destFull -Force -ErrorAction Stop
# Для надёжности можно проверить существование файла на шаре и размер:
if (Test-Path -Path $destFull) {
$srcSize = (Get-Item -LiteralPath $srcFull -ErrorAction Stop).Length
$dstSize = (Get-Item -LiteralPath $destFull -ErrorAction Stop).Length
if ($srcSize -eq $dstSize) {
$copied = $true
Write-Log "Файл успешно скопирован и проверен (размеры совпадают): $f.Name" 'INFO'
} else {
Write-Log "Предупреждение: размеры не совпадают (src=$srcSize, dst=$dstSize). Попытка повторить." 'WARN'
Start-Sleep -Seconds $RetryDelaySeconds
}
} else {
Write-Log "Файл не найден на шаре после Copy-Item. Попытка повторить." 'WARN'
Start-Sleep -Seconds $RetryDelaySeconds
}
} catch {
Write-Log "Ошибка при копировании файла '$srcFull': $_" 'ERROR'
if ($attempt -lt $MaxAttempts) {
Write-Log "Ожидание $RetryDelaySeconds сек. и повтор." 'DEBUG'
Start-Sleep -Seconds $RetryDelaySeconds
}
}
} # end while
if (-not $copied) {
Write-Log "НЕ удалось скопировать файл после $MaxAttempts попыток: $srcFull" 'ERROR'
$failedFiles += $f.Name
# Продолжаем попытки копирования остальных файлов (можно прервать сразу, но мы собираем все ошибки)
}
} # end foreach
# 5) Обработка результатов копирования
if ($failedFiles.Count -gt 0) {
$subject = "Ошибка копирования"
$body = "При копировании файлов возникли ошибки.`nНеисправные файлы:`n" + ($failedFiles -join "`n")
Write-Log "Копирование завершилось с ошибками. Не перемещаем исходники. Формируем уведомление." 'ERROR'
Send-Notification -Subject $subject -Body $body
exit 5
}
Write-Log "Все файлы успешно скопированы на $DestShare." 'INFO'
# 6) Архивирование (перемещение исходников)
# Формируем папку архива формата yyyy-MM-dd
$archiveDateFolder = (Get-Date).ToString('yyyy-MM-dd')
$archiveDir = Join-Path -Path $ArchiveRoot -ChildPath $archiveDateFolder
if (-not (Test-Path -Path $archiveDir)) {
Write-Log "Создаём архивную папку: $archiveDir" 'DEBUG'
try {
New-Item -Path $archiveDir -ItemType Directory -Force | Out-Null
} catch {
Write-Log "ОШИБКА: не удалось создать папку архива $archiveDir: $_" 'ERROR'
Send-Notification -Subject "Ошибка архивации" -Body "Не удалось создать папку архива $archiveDir. Ошибка: $_"
exit 6
}
}
# Перемещаем файлы (Move-Item)
$movedCount = 0
$moveErrors = @()
foreach ($f in $files) {
$srcFull = $f.FullName
$destFull = Join-Path -Path $archiveDir -ChildPath $f.Name
try {
Write-Log "Перемещение файла '$srcFull' -> '$destFull'." 'DEBUG'
Move-Item -Path $srcFull -Destination $destFull -Force -ErrorAction Stop
$movedCount++
} catch {
Write-Log "ОШИБКА при перемещении файла '$srcFull' в архив: $_" 'ERROR'
$moveErrors += $f.Name
}
}
if ($moveErrors.Count -gt 0) {
Write-Log "Некоторые файлы не удалось переместить в архив: $($moveErrors -join ', ')" 'ERROR'
# Несмотря на проблему с перемещением, отправим уведомление об ошибке архивации
$subject = "Ошибка архивации"
$body = "Файлы были успешно скопированы на $DestShare, но при перемещении в архив возникли ошибки.`nНе перемещены:`n" + ($moveErrors -join "`n")
Send-Notification -Subject $subject -Body $body
# В зависимости от требований можно попытаться откатить, но по ТЗ просто уведомляем и выходим
exit 7
}
Write-Log "Успешно перемещено файлов в архив: $movedCount. Архив: $archiveDir" 'INFO'
# 7) Отправка успешного уведомления
$subject = "Выписки"
$body = "Успешно скопировано $fileCount файлов в $DestShare.`nФайлы перемещены в архив: $archiveDir"
Send-Notification -Subject $subject -Body $body
Write-Log "Операция выполнена успешно. Завершение." 'INFO'
exit 0
} catch {
# Ловим неожиданные исключения
Write-Log "НЕОЖИДАННАЯ ОШИБКА: $_" 'ERROR'
try {
Send-Notification -Subject "Ошибка синхронизации (исключение)" -Body "При выполнении скрипта возникло исключение: `n$_"
} catch {
Write-Log "Не удалось отправить уведомление об исключении: $_" 'ERROR'
}
exit 99
} finally {
# Очистка: освобождение Mutex (если не было раньше)
try {
if ($mutexAcquired -and $global:mutex -ne $null) {
$global:mutex.ReleaseMutex() | Out-Null
$global:mutex.Dispose()
Write-Log "Mutex освобождён (в finally)." 'DEBUG'
}
} catch {
Write-Log "Ошибка при освобождении Mutex в finally: $_" 'ERROR'
}
Write-Log "=== Конец выполнения скрипта ===" 'INFO'
}
<#
.SYNOPSIS
Синхронизация выписок: проверка рабочего дня, порога файлов, копирование на сетевой шар и архивирование локально с подробным логированием и уведомлениями по SMTP.
.DESCRIPTION
Скрипт предназначен для запуска в Task Scheduler на Windows Server 2016 (PowerShell 5.1).
Работает по следующему алгоритму:
1. Блокировка (предотвращение параллельных запусков).
2. Создание каталога логов и файла лога для текущего запуска.
3. Проверка — сегодня рабочий день (Пн–Пт). Если нет — выход.
4. Подсчёт файлов в c:\statement\file (только корень, не рекурсивно). Исключаем c:\statement\arc.
5. Если файлов
#region ========== Настройки (отредактируйте перед запуском) ==========
# Пути
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$LogsRoot = Join-Path -Path $ScriptPath -ChildPath 'logs' # c:\script\logs (по умолчанию)
$SourceDir = 'C:\statement\file' # исходная папка с файлами (корень)
$ArchiveRoot = 'C:\statement\arc' # корень архива; внутри создаётся yyyy-MM-dd
# Сетевой ресурс назначения
$DestShare = '\\rs144\statement' # сетевой шар назначения
# Порог файлов
$MinFilesThreshold = 40 # продолжать только если count > 40
# Копирование/повторы
$MaxAttempts = 3 # число попыток для копирования каждого файла
$RetryDelaySeconds = 30 # пауза между попытками
# Блокировка параллельных запусков (название Mutex)
$MutexName = 'Global\SyncStatementsMutex_Predator89' # можно изменить, если нужно
# SMTP (заполните перед использованием)
$SmtpHost = 'smtp.example.local' # <-- УКАЖИТЕ SMTP-сервер
$SmtpPort = 25 # <-- УКАЖИТЕ порт
$MailFrom = 'noreply@example.local' # <-- УКАЖИТЕ From
$MailTo = 'ops@example.local' # <-- УКАЖИТЕ To (можно CSV: 'a@b,c@d')
# Флаги поведения
$RequireAllFilesCopied = $true # если $true, при любой ошибке копирования НЕ перемещаем исходники
#endregion =============================================================
#region ========== Функции логирования и уведомлений ==========
# Создание файла лога для текущего запуска: logs\sync_YYYY-MM-DD_HH-mm-ss.log
if (-not (Test-Path -Path $LogsRoot)) {
New-Item -Path $LogsRoot -ItemType Directory -Force | Out-Null
}
$timeStampForFile = (Get-Date).ToString('yyyy-MM-dd_HH-mm-ss')
$LogFile = Join-Path -Path $LogsRoot -ChildPath ("sync_$timeStampForFile.log")
function Write-Log {
param(
[string]$Message,
[ValidateSet('INFO','WARN','ERROR','DEBUG')][string]$Level = 'INFO'
)
$ts = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
$line = "[$ts] [$Level] $Message"
# Пишем в файл лога и в консоль (Task Scheduler видит стандартный вывод в историю)
try {
$line | Out-File -FilePath $LogFile -Encoding UTF8 -Append -Force
} catch {
# Если не удалось записать в лог - хоть что-то вывести на консоль
Write-Output "Ошибка записи в лог: $_"
}
Write-Output $line
}
function Send-Notification {
param(
[string]$Subject,
[string]$Body
)
Write-Log "Отправка уведомления по SMTP: Subject='$Subject' To='$MailTo'" 'INFO'
try {
# Send-MailMessage используется для PS 5.1; предполагаем открытый SMTP без авторизации
Send-MailMessage -SmtpServer $SmtpHost -Port $SmtpPort -From $MailFrom -To $MailTo -Subject $Subject -Body $Body -BodyAsHtml:$false -ErrorAction Stop
Write-Log "Уведомление отправлено успешно." 'INFO'
} catch {
Write-Log "ОШИБКА при отправке уведомления: $_" 'ERROR'
}
}
#endregion ================================================================
#region ========== Блокировка (Mutex) ==========
# Предотвращаем параллельные запуски с помощью именованного Mutex
$mutexAcquired = $false
try {
Write-Log "Попытка создать/получить Mutex '$MutexName' чтобы предотвратить параллельные запуски." 'DEBUG'
$global:mutex = New-Object System.Threading.Mutex($false, $MutexName)
# Попробуем захватить немедленно (0 ms)
$mutexAcquired = $global:mutex.WaitOne(0)
if (-not $mutexAcquired) {
Write-Log "Другой экземпляр уже запущен. Выход." 'WARN'
exit 2
} else {
Write-Log "Mutex захвачен. Продолжаем выполнение." 'DEBUG'
}
} catch {
Write-Log "ОШИБКА при создании/захвате Mutex: $_" 'ERROR'
# Если не можем создать mutex — не запускаем, чтобы избежать гонок
exit 3
}
#endregion ================================================================
# Обработчик завершения — освободить Mutex
$cleanup = {
if ($global:mutex -ne $null) {
try {
$global:mutex.ReleaseMutex() | Out-Null
$global:mutex.Dispose()
Write-Log "Mutex освобождён." 'DEBUG'
} catch {
Write-Log "Ошибка при освобождении Mutex: $_" 'ERROR'
}
}
}
# Регистрируем cleanup на выход
Register-EngineEvent PowerShell.Exiting -Action { & $cleanup } | Out-Null
# Главный try/catch чтобы везде логировать ошибки
try {
Write-Log "=== Запуск синхронизации выписок ===" 'INFO'
Write-Log "Параметры: SourceDir='$SourceDir', DestShare='$DestShare', ArchiveRoot='$ArchiveRoot', LogFile='$LogFile'" 'DEBUG'
# 1) Проверка рабочего дня (Пн-Пт)
$dow = (Get-Date).DayOfWeek
Write-Log "Текущий день недели: $dow" 'DEBUG'
if ($dow -eq 'Saturday' -or $dow -eq 'Sunday') {
Write-Log "Суббота/Воскресенье — не рабочий день. Завершение." 'INFO'
exit 0
}
Write-Log "Сегодня рабочий день — продолжаем." 'INFO'
# 2) Подсчёт файлов в SourceDir (только корень, файлы, не рекурсивно)
if (-not (Test-Path -Path $SourceDir)) {
Write-Log "Исходная папка не найдена: $SourceDir" 'ERROR'
Send-Notification -Subject "Ошибка синхронизации: Исходная папка не найдена" -Body "Исходная папка $SourceDir не найдена на сервере."
exit 4
}
# Получаем только файлы (не папки), не рекурсивно
$files = Get-ChildItem -Path $SourceDir -File -Force -ErrorAction Stop
$fileCount = $files.Count
Write-Log "Найдено файлов в '$SourceDir' (корень): $fileCount" 'INFO'
# 3) Проверка порога
if ($fileCount -le $MinFilesThreshold) {
$subject = "Файлов мало"
$body = "Сформировано мало файлов: $fileCount. Требуется $($MinFilesThreshold + 1) или более для отправки."
Write-Log "Файлов меньше или равно порога ($MinFilesThreshold). Отправляем уведомление и выходим." 'WARN'
Send-Notification -Subject $subject -Body $body
exit 0
}
# 4) Копирование файлов на сетевой шар
Write-Log "Начинаем копирование $fileCount файлов в '$DestShare'." 'INFO'
$failedFiles = @()
foreach ($f in $files) {
$srcFull = $f.FullName
$destFull = Join-Path -Path $DestShare -ChildPath $f.Name
$attempt = 0
$copied = $false
while (($attempt -lt $MaxAttempts) -and (-not $copied)) {
$attempt++
try {
Write-Log "Копирование файла '$srcFull' -> '$destFull' (попытка $attempt из $MaxAttempts)." 'DEBUG'
# Copy-Item с -Force перезапишет, но по ТЗ конфликтов не ожидается
Copy-Item -Path $srcFull -Destination $destFull -Force -ErrorAction Stop
# Для надёжности можно проверить существование файла на шаре и размер:
if (Test-Path -Path $destFull) {
$srcSize = (Get-Item -LiteralPath $srcFull -ErrorAction Stop).Length
$dstSize = (Get-Item -LiteralPath $destFull -ErrorAction Stop).Length
if ($srcSize -eq $dstSize) {
$copied = $true
Write-Log "Файл успешно скопирован и проверен (размеры совпадают): $f.Name" 'INFO'
} else {
Write-Log "Предупреждение: размеры не совпадают (src=$srcSize, dst=$dstSize). Попытка повторить." 'WARN'
Start-Sleep -Seconds $RetryDelaySeconds
}
} else {
Write-Log "Файл не найден на шаре после Copy-Item. Попытка повторить." 'WARN'
Start-Sleep -Seconds $RetryDelaySeconds
}
} catch {
Write-Log "Ошибка при копировании файла '$srcFull': $_" 'ERROR'
if ($attempt -lt $MaxAttempts) {
Write-Log "Ожидание $RetryDelaySeconds сек. и повтор." 'DEBUG'
Start-Sleep -Seconds $RetryDelaySeconds
}
}
} # end while
if (-not $copied) {
Write-Log «НЕ удалось скопировать файл после $MaxAttempts попыток: $srcFull» ‘ERROR’
$failedFiles += $f.Name
# Продолжаем попытки копирования остальных файлов (можно прервать сразу, но мы собираем все ошибки)
}
} # end foreach
# 5) Обработка результатов копирования
if ($failedFiles.Count -gt 0) {
$subject = «Ошибка копирования»
$body = «При копировании файлов возникли ошибки.`nНеисправные файлы:`n» + ($failedFiles -join «`n»)
Write-Log «Копирование завершилось с ошибками. Не перемещаем исходники. Формируем уведомление.» ‘ERROR’
Send-Notification -Subject $subject -Body $body
exit 5
}
Write-Log «Все файлы успешно скопированы на $DestShare.» ‘INFO’
# 6) Архивирование (перемещение исходников)
# Формируем папку архива формата yyyy-MM-dd
$archiveDateFolder = (Get-Date).ToString(‘yyyy-MM-dd’)
$archiveDir = Join-Path -Path $ArchiveRoot -ChildPath $archiveDateFolder
if (-not (Test-Path -Path $archiveDir)) {
Write-Log «Создаём архивную папку: $archiveDir» ‘DEBUG’
try {
New-Item -Path $archiveDir -ItemType Directory -Force | Out-Null
} catch {
Write-Log «ОШИБКА: не удалось создать папку архива $archiveDir: $_» ‘ERROR’
Send-Notification -Subject «Ошибка архивации» -Body «Не удалось создать папку архива $archiveDir. Ошибка: $_»
exit 6
}
}
# Перемещаем файлы (Move-Item)
$movedCount = 0
$moveErrors = @()
foreach ($f in $files) {
$srcFull = $f.FullName
$destFull = Join-Path -Path $archiveDir -ChildPath $f.Name
try {
Write-Log «Перемещение файла ‘$srcFull’ -> ‘$destFull’.» ‘DEBUG’
Move-Item -Path $srcFull -Destination $destFull -Force -ErrorAction Stop
$movedCount++
} catch {
Write-Log «ОШИБКА при перемещении файла ‘$srcFull’ в архив: $_» ‘ERROR’
$moveErrors += $f.Name
}
}
if ($moveErrors.Count -gt 0) {
Write-Log «Некоторые файлы не удалось переместить в архив: $($moveErrors -join ‘, ‘)» ‘ERROR’
# Несмотря на проблему с перемещением, отправим уведомление об ошибке архивации
$subject = «Ошибка архивации»
$body = «Файлы были успешно скопированы на $DestShare, но при перемещении в архив возникли ошибки.`nНе перемещены:`n» + ($moveErrors -join «`n»)
Send-Notification -Subject $subject -Body $body
# В зависимости от требований можно попытаться откатить, но по ТЗ просто уведомляем и выходим
exit 7
}
Write-Log «Успешно перемещено файлов в архив: $movedCount. Архив: $archiveDir» ‘INFO’
# 7) Отправка успешного уведомления
$subject = «Выписки»
$body = «Успешно скопировано $fileCount файлов в $DestShare.`nФайлы перемещены в архив: $archiveDir»
Send-Notification -Subject $subject -Body $body
Write-Log «Операция выполнена успешно. Завершение.» ‘INFO’
exit 0
} catch {
# Ловим неожиданные исключения
Write-Log «НЕОЖИДАННАЯ ОШИБКА: $_» ‘ERROR’
try {
Send-Notification -Subject «Ошибка синхронизации (исключение)» -Body «При выполнении скрипта возникло исключение: `n$_»
} catch {
Write-Log «Не удалось отправить уведомление об исключении: $_» ‘ERROR’
}
exit 99
} finally {
# Очистка: освобождение Mutex (если не было раньше)
try {
if ($mutexAcquired -and $global:mutex -ne $null) {
$global:mutex.ReleaseMutex() | Out-Null
$global:mutex.Dispose()
Write-Log «Mutex освобождён (в finally).» ‘DEBUG’
}
} catch {
Write-Log «Ошибка при освобождении Mutex в finally: $_» ‘ERROR’
}
Write-Log «=== Конец выполнения скрипта ===» ‘INFO’
}
.SYNOPSIS
Синхронизация выписок: проверка рабочего дня, порога файлов, копирование на сетевой шар и архивирование локально с подробным логированием и уведомлениями по SMTP.
.DESCRIPTION
Скрипт предназначен для запуска в Task Scheduler на Windows Server 2016 (PowerShell 5.1).
Работает по следующему алгоритму:
1. Блокировка (предотвращение параллельных запусков).
2. Создание каталога логов и файла лога для текущего запуска.
3. Проверка — сегодня рабочий день (Пн–Пт). Если нет — выход.
4. Подсчёт файлов в c:\statement\file (только корень, не рекурсивно). Исключаем c:\statement\arc.
5. Если файлов <= 40 — отправляем уведомление "Файлов мало" по SMTP и выходим.
6. Копируем все файлы в \\rs144\statement с повторными попытками при ошибках (3 попытки, пауза 30 сек).
Требуется, чтобы все файлы были успешно скопированы; при любой ошибке — отправляем письмо "Ошибка копирования" и выходим, не перемещая исходники.
7. При полном успехе создаём архивную папку c:\statement\arc\yyyy-MM-dd (если нет) и перемещаем туда исходные файлы (Move).
8. Отправляем письмо об успешной операции с количеством файлов и завершаем работу.
9. Логируем подробно все шаги, ошибки и результат в отдельный лог-файл с датой/временем в имени.
.PARAMETERS
(настройки в верхней части скрипта)
.NOTES
— Скрипт предполагает, что учётная запись, под которой запускается задача, имеет доступ к \\rs144\statement.
— SMTP: указать хост/порт/From/To в переменных.
— Разработан для PowerShell 5.1 (Server 2016).
#region ========== Подготовка окружения ==========
# <<< CHANGED: установить кодировку вывода консоли в UTF-8, чтобы лог в консоль корректно отображал кириллицу
try {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
} catch {
# Игнорируем ошибки на старых версиях PowerShell, но обычно работает
}
#endregion ================================================================
#region ========== Функции логирования и уведомлений ==========
# Создание файла лога для текущего запуска: logs\sync_YYYY-MM-DD_HH-mm-ss.log
if (-not (Test-Path -Path $LogsRoot)) {
New-Item -Path $LogsRoot -ItemType Directory -Force | Out-Null
}
$timeStampForFile = (Get-Date).ToString('yyyy-MM-dd_HH-mm-ss')
$LogFile = Join-Path -Path $LogsRoot -ChildPath ("sync_$timeStampForFile.log")
function Write-Log {
param(
[string]$Message,
[ValidateSet('INFO','WARN','ERROR','DEBUG')][string]$Level = 'INFO'
)
$ts = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
$line = "[$ts] [$Level] $Message"
# Пишем в файл лога и в консоль (Task Scheduler видит стандартный вывод в историю)
try {
$line | Out-File -FilePath $LogFile -Encoding UTF8 -Append -Force
} catch {
# Если не удалось записать в лог - хоть что-то вывести на консоль
Write-Output "Ошибка записи в лог: $_"
}
Write-Output $line
}
# <<< CHANGED: Заменили реализацию Send-Notification на MailMessage с явной кодировкой UTF-8 (более надёжно для кириллицы)
function Send-Notification {
param(
[string]$Subject,
[string]$Body
)
Write-Log "Отправка уведомления по SMTP (MailMessage): Subject='$Subject' To='$MailTo'" 'INFO'
$mail = $null
$client = $null
try {
$mail = New-Object System.Net.Mail.MailMessage
# From
try {
$mail.From = New-Object System.Net.Mail.MailAddress($MailFrom)
} catch {
Write-Log "Неверный формат адреса отправителя ($MailFrom): $_" 'ERROR'
throw
}
# To (поддержка нескольких адресов, разделённых запятой/точкой с запятой)
foreach ($addr in ($MailTo -split '[,;]')) {
$trimmed = $addr.Trim()
if ($trimmed) {
try {
$mail.To.Add($trimmed)
} catch {
Write-Log "Неверный адрес получателя: $trimmed. Ошибка: $_" 'ERROR'
}
}
}
$mail.Subject = $Subject
$mail.Body = $Body
$mail.IsBodyHtml = $false
# Явно задаём кодировки UTF-8 для темы и тела
$mail.BodyEncoding = [System.Text.Encoding]::UTF8
$mail.SubjectEncoding = [System.Text.Encoding]::UTF8
# Дополнительно укажем Content-Type header с charset (на всякий случай)
try {
if (-not $mail.Headers['Content-Type']) {
$mail.Headers.Add('Content-Type', 'text/plain; charset=utf-8')
}
} catch {
# Некоторые реализации не позволяют менять header Content-Type напрямую — игнорируем ошибку
Write-Log "Не удалось явно добавить заголовок Content-Type: $_" 'DEBUG'
}
$client = New-Object System.Net.Mail.SmtpClient($SmtpHost, [int]$SmtpPort)
# Без авторизации (по ТЗ). Если понадобится — можно задать $client.Credentials и $client.EnableSsl.
$client.Timeout = 300000 # 5 минут
$client.Send($mail)
Write-Log "Уведомление отправлено успешно (MailMessage)." 'INFO'
} catch {
Write-Log "ОШИБКА при отправке уведомления (MailMessage): $_" 'ERROR'
} finally {
if ($mail -ne $null) { $mail.Dispose() }
if ($client -ne $null) { $client.Dispose() }
}
}
#endregion ================================================================
# Шаблоны тела писем (используются маркеры {NAME})$Template_Success = "Успешно скопировано {COUNT} файлов в {DEST}.`nФайлы перемещены в архив: {ARCHIVE}."
$Template_TooFew = "Сформировано мало файлов: {COUNT}. Необходимое количество: {MIN}."
$Template_CopyErr = "При копировании возникли ошибки.`nНе удалось скопировать файлы:`n{FAILED_LIST}"
$Template_ArchiveErr = "Файлы были скопированы, но при перемещении в архив возникли ошибки:`n{FAILED_LIST}"
$Template_Exception = "При выполнении скрипта возникло исключение:`n{ERROR}"
$Template_SourceMiss = "Исходная папка не найдена: {PATH}"
function Send-Notification {param(
[string]$Subject,
[string]$Body
)
Write-Log "Отправка уведомления по SMTP (MailMessage): Subject='$Subject' To='$MailTo'" 'INFO'
$mail = $null
$client = $null
try {
$mail = New-Object System.Net.Mail.MailMessage
$mail.From = New-Object System.Net.Mail.MailAddress($MailFrom)
foreach ($addr in ($MailTo -split '[,;]')) {
$trimmed = $addr.Trim()
if ($trimmed) {
try { $mail.To.Add($trimmed) } catch { Write-Log "Неверный адрес получателя: $trimmed. $_" 'ERROR' }
}
}
$mail.Subject = $Subject
$mail.Body = $Body
$mail.IsBodyHtml = $false
$mail.BodyEncoding = [System.Text.Encoding]::UTF8
$mail.SubjectEncoding = [System.Text.Encoding]::UTF8
try { if (-not $mail.Headers['Content-Type']) { $mail.Headers.Add('Content-Type', 'text/plain; charset=utf-8') } } catch {}
$client = New-Object System.Net.Mail.SmtpClient($SmtpHost, [int]$SmtpPort)
$client.Timeout = 300000 # 5 минут
$client.Send($mail)
Write-Log "Уведомление отправлено успешно (MailMessage)." 'INFO'
} catch {
Write-Log "ОШИБКА при отправке уведомления (MailMessage): $_" 'ERROR'
} finally {
if ($mail -ne $null) { $mail.Dispose() }
if ($client -ne $null) { $client.Dispose() }
}
}
# ---------------------------------------------------------------------------------------
# <<< ADD: Функция Expand-Template (добавлено) -------------------------------------------
function Expand-Template {
param(
[string]$Template,
[hashtable]$Vars
)
if ($Vars -ne $null) {
foreach ($k in $Vars.Keys) {
$Template = $Template.Replace("{$k}", [string]$Vars[$k])
}
}
return $Template
}
# ---------------------------------------------------------------------------------------
# <<< ADD: Централизованная отправка по статусу (добавлено) ------------------------------
function Send-FinalStatus {
param(
[ValidateSet('SUCCESS','TOO_FEW','COPY_ERROR','ARCHIVE_ERROR','EXCEPTION','SOURCE_MISSING')] $Status,
[hashtable]$Data
)
switch ($Status) {
'SUCCESS' { $body = Expand-Template $Template_Success $Data }
'TOO_FEW' { $body = Expand-Template $Template_TooFew $Data }
'COPY_ERROR' { $body = Expand-Template $Template_CopyErr $Data }
'ARCHIVE_ERROR' { $body = Expand-Template $Template_ArchiveErr $Data }
'EXCEPTION' { $body = Expand-Template $Template_Exception $Data }
'SOURCE_MISSING' { $body = Expand-Template $Template_SourceMiss $Data }
default { $body = "Неизвестный статус: $Status" }
}
Write-Log "Отправка итогового уведомления. Subject='$CommonMailSubject'. Body (preview): $([string]$body.Substring(0, [Math]::Min(200, $body.Length)))" 'DEBUG'
Send-Notification -Subject $CommonMailSubject -Body $body
}
# ---------------------------------------------------------------------------------------
using System;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;
using System.Threading;
public class FormsService : ServiceBase
{
private Process child;
private Thread monitorThread;
private volatile bool stopping = false;
private string exePath = "";
private string exeArgs = "";
private string workDir = "";
private int restartDelaySec = 5;
private string logFile = @"C:\opt\forms\logs\FormsService.log";
protected override void OnStart(string[] args)
{
try
{
// читаем параметры командной строки: --exe="C:\path\run.exe" --args="..." --workDir="C:\..." --delay=5 --log="C:\..."
string[] cmd = Environment.GetCommandLineArgs();
for (int i = 1; i < cmd.Length; i++)
{
string s = cmd[i];
if (s.StartsWith("--exe=", StringComparison.OrdinalIgnoreCase)) exePath = StripQuotes(s.Substring(6));
else if (s.StartsWith("--args=", StringComparison.OrdinalIgnoreCase)) exeArgs = StripQuotes(s.Substring(7));
else if (s.StartsWith("--workDir=", StringComparison.OrdinalIgnoreCase)) workDir = StripQuotes(s.Substring(10));
else if (s.StartsWith("--delay=", StringComparison.OrdinalIgnoreCase)) int.TryParse(StripQuotes(s.Substring(8)), out restartDelaySec);
else if (s.StartsWith("--log=", StringComparison.OrdinalIgnoreCase)) logFile = StripQuotes(s.Substring(6));
}
if (string.IsNullOrEmpty(exePath))
{
Log("Missing --exe parameter. Service will not start process.");
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(logFile) ?? @"C:\opt\forms\logs");
if (string.IsNullOrEmpty(workDir)) workDir = Path.GetDirectoryName(exePath);
stopping = false;
monitorThread = new Thread(MonitorLoop) { IsBackground = true, Name = "FormsService-Monitor" };
monitorThread.Start();
Log("Service started. Monitoring process: " + exePath + " " + exeArgs);
}
}
catch (Exception ex)
{
Log("OnStart error: " + ex);
throw;
}
}
private void MonitorLoop()
{
while (!stopping)
{
try
{
StartChild();
if (child != null)
{
child.WaitForExit();
Log("Child exited with code " + child.ExitCode);
}
}
catch (ThreadAbortException) { break; }
catch (Exception ex)
{
Log("MonitorLoop exception: " + ex);
}
if (stopping) break;
Log($"Restarting in {restartDelaySec} sec...");
for (int i = 0; i { /* handled by WaitForExit */ };
bool ok = child.Start();
Log("Started child process. PID=" + (ok ? child.Id.ToString() : "unknown"));
}
catch (Exception ex)
{
Log("StartChild failed: " + ex);
// если не удалось запустить — подождём дольше перед новой попыткой
Thread.Sleep(60000);
}
}
protected override void OnStop()
{
try
{
stopping = true;
if (child != null && !child.HasExited)
{
try
{
child.Kill();
child.WaitForExit(5000);
Log("Child killed on service stop.");
}
catch (Exception ex) { Log("Error killing child: " + ex); }
}
if (monitorThread != null && monitorThread.IsAlive)
{
monitorThread.Join(5000);
}
Log("Service stopped.");
}
catch (Exception ex)
{
Log("OnStop exception: " + ex);
}
}
private void Log(string msg)
{
try
{
string line = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " - " + msg;
File.AppendAllText(logFile, line + Environment.NewLine);
}
catch { /* best-effort logging */ }
try
{
// пишем в Application Event Log (если возможно)
if (!EventLog.SourceExists("FormsService")) EventLog.CreateEventSource("FormsService", "Application");
EventLog.WriteEntry("FormsService", msg, EventLogEntryType.Information);
}
catch { }
}
private static string StripQuotes(string s)
{
if (s.StartsWith("\"") && s.EndsWith("\"")) return s.Substring(1, s.Length - 2);
return s;
}
public static void Main()
{
ServiceBase.Run(new FormsService());
}
}
$ErrorActionPreference = 'Stop'
# Функция для логирования подробностей исключения
function Log-ExceptionDetails {
param($ex)
try {
$msg = $ex.Exception.Message
$inv = $ex.InvocationInfo
$scriptLine = $inv.ScriptLineNumber
$position = $inv.PositionMessage
$command = $inv.MyCommand.Name
$stack = $ex.ScriptStackTrace
Write-Log ("EXCEPTION MESSAGE: {0}" -f $msg) 'ERROR'
Write-Log ("INVOCATION: Command={0} ScriptLine={1} Position={2}" -f $command, $scriptLine, $position) 'DEBUG'
if ($stack) { Write-Log ("SCRIPT STACK TRACE: {0}" -f $stack) 'DEBUG' }
} catch {
Write-Log ("Ошибка при логировании исключения: {0}" -f $_) 'ERROR'
}
}
using System;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;
using System.Threading;
public class FormsService : ServiceBase
{
private Process child;
private Thread monitorThread;
private volatile bool stopping = false;
private string exePath = "";
private string exeArgs = "";
private string workDir = "";
private int restartDelaySec = 5;
private string logFile = @"C:\opt\forms\logs\FormsService.log";
protected override void OnStart(string[] args)
{
try
{
// читаем параметры командной строки: --exe="C:\path\run.exe" --args="..." --workDir="C:\..." --delay=5 --log="C:\..."
string[] cmd = Environment.GetCommandLineArgs();
for (int i = 1; i < cmd.Length; i++)
{
string s = cmd[i];
if (s.StartsWith("--exe=", StringComparison.OrdinalIgnoreCase)) exePath = StripQuotes(s.Substring(6));
else if (s.StartsWith("--args=", StringComparison.OrdinalIgnoreCase)) exeArgs = StripQuotes(s.Substring(7));
else if (s.StartsWith("--workDir=", StringComparison.OrdinalIgnoreCase)) workDir = StripQuotes(s.Substring(10));
else if (s.StartsWith("--delay=", StringComparison.OrdinalIgnoreCase)) int.TryParse(StripQuotes(s.Substring(8)), out restartDelaySec);
else if (s.StartsWith("--log=", StringComparison.OrdinalIgnoreCase)) logFile = StripQuotes(s.Substring(6));
}
if (string.IsNullOrEmpty(exePath))
{
Log("Missing --exe parameter. Service will not start process.");
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(logFile) ?? @"C:\opt\forms\logs");
if (string.IsNullOrEmpty(workDir)) workDir = Path.GetDirectoryName(exePath);
stopping = false;
monitorThread = new Thread(MonitorLoop) { IsBackground = true, Name = "FormsService-Monitor" };
monitorThread.Start();
Log("Service started. Monitoring process: " + exePath + " " + exeArgs);
}
}
catch (Exception ex)
{
Log("OnStart error: " + ex);
throw;
}
}
private void MonitorLoop()
{
while (!stopping)
{
try
{
StartChild();
if (child != null)
{
child.WaitForExit();
Log("Child exited with code " + child.ExitCode);
}
}
catch (ThreadAbortException) { break; }
catch (Exception ex)
{
Log("MonitorLoop exception: " + ex);
}
if (stopping) break;
Log($"Restarting in {restartDelaySec} sec...");
for (int i = 0; i { /* handled by WaitForExit */ };
bool ok = child.Start();
Log("Started child process. PID=" + (ok ? child.Id.ToString() : "unknown"));
}
catch (Exception ex)
{
Log("StartChild failed: " + ex);
// если не удалось запустить — подождём дольше перед новой попыткой
Thread.Sleep(60000);
}
}
protected override void OnStop()
{
try
{
stopping = true;
if (child != null && !child.HasExited)
{
try
{
child.Kill();
child.WaitForExit(5000);
Log("Child killed on service stop.");
}
catch (Exception ex) { Log("Error killing child: " + ex); }
}
if (monitorThread != null && monitorThread.IsAlive)
{
monitorThread.Join(5000);
}
Log("Service stopped.");
}
catch (Exception ex)
{
Log("OnStop exception: " + ex);
}
}
private void Log(string msg)
{
try
{
string line = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " - " + msg;
File.AppendAllText(logFile, line + Environment.NewLine);
}
catch { /* best-effort logging */ }
try
{
// пишем в Application Event Log (если возможно)
if (!EventLog.SourceExists("FormsService")) EventLog.CreateEventSource("FormsService", "Application");
EventLog.WriteEntry("FormsService", msg, EventLogEntryType.Information);
}
catch { }
}
private static string StripQuotes(string s)
{
if (s.StartsWith("\"") && s.EndsWith("\"")) return s.Substring(1, s.Length - 2);
return s;
}
public static void Main()
{
ServiceBase.Run(new FormsService());
}
}