Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88f8819570 | ||
|
|
9288fc95cb | ||
|
|
beced42f2f |
@@ -49,19 +49,33 @@ jobs:
|
|||||||
# electron-builder.yml просто копирует resources/zapret, а он у чистого чекаута пуст.
|
# electron-builder.yml просто копирует resources/zapret, а он у чистого чекаута пуст.
|
||||||
- run: node scripts/fetch-zapret.mjs
|
- run: node scripts/fetch-zapret.mjs
|
||||||
|
|
||||||
|
# electron-builder заливает .exe и .blockmap двумя параллельными publisher'ами: оба
|
||||||
|
# проверяют, есть ли релиз с этим тегом, оба видят «нет» и оба лезут его создавать —
|
||||||
|
# второй падает с 422 already_exists и уносит с собой свой файл (на v0.5.0 так потерялся
|
||||||
|
# сам .exe). Создаём релиз заранее, тогда обоим остаётся только залить в готовый.
|
||||||
|
# Описание пустое — его заполняет шаг «Описание релиза» после сборки.
|
||||||
|
- name: Заготовка релиза
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 ||
|
||||||
|
gh release create "$GITHUB_REF_NAME" --title "${GITHUB_REF_NAME#v}" --notes ''
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# Подпись не настроена: артефакты неподписанные.
|
# Подпись не настроена: артефакты неподписанные.
|
||||||
# Чтобы подписывать — добавьте секреты CSC_LINK, CSC_KEY_PASSWORD и передайте их в env.
|
# Чтобы подписывать — добавьте секреты CSC_LINK, CSC_KEY_PASSWORD и передайте их в env.
|
||||||
#
|
#
|
||||||
# На пуш тега vX.Y.Z (см. on.push.tags выше) electron-builder публикует .exe в GitHub
|
# На пуш тега vX.Y.Z (см. on.push.tags выше) electron-builder заливает .exe в релиз,
|
||||||
# Release с этим тегом (создаёт его, если нет), используя repository из package.json.
|
# заготовленный шагом выше, используя repository из package.json.
|
||||||
# На push в main/PR публикации нет — только упаковка и загрузка в Artifacts ниже.
|
# На push в main/PR публикации нет — только упаковка и загрузка в Artifacts ниже.
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npx --no-install electron-vite build && npx --no-install electron-builder --publish ${{ startsWith(github.ref, 'refs/tags/v') && 'always' || 'never' }}
|
run: npx --no-install electron-vite build && npx --no-install electron-builder --publish ${{ startsWith(github.ref, 'refs/tags/v') && 'always' || 'never' }}
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# electron-builder создаёт релиз с пустым описанием, поэтому список изменений
|
# Релиз создан с пустым описанием, поэтому список изменений дописываем сами:
|
||||||
# дописываем сами: коммиты между предыдущим тегом vX.Y.Z и текущим.
|
# коммиты между предыдущим тегом vX.Y.Z и текущим.
|
||||||
- name: Описание релиза
|
- name: Описание релиза
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "rknboost",
|
"name": "rknboost",
|
||||||
"version": "0.5.0",
|
"version": "0.5.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "rknboost",
|
"name": "rknboost",
|
||||||
"version": "0.5.0",
|
"version": "0.5.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rknboost",
|
"name": "rknboost",
|
||||||
"version": "0.5.0",
|
"version": "0.5.1",
|
||||||
"description": "Кроссплатформенное десктопное приложение на Electron + React + TypeScript",
|
"description": "Кроссплатформенное десктопное приложение на Electron + React + TypeScript",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "RKNboost",
|
"author": "RKNboost",
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { execFile } from 'node:child_process'
|
import { execFile } from 'node:child_process'
|
||||||
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
import { createHash } from 'node:crypto'
|
||||||
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import { promisify } from 'node:util'
|
import { promisify } from 'node:util'
|
||||||
import type { ZapretStatus } from '../../shared/ipc-contract'
|
import type { ZapretStatus } from '../../shared/ipc-contract'
|
||||||
import { ZapretEngine } from './engine'
|
import { ZapretEngine } from './engine'
|
||||||
import { assertExecutable, protectedWinwsPath, serviceConfigPath, winwsPath } from './paths'
|
import {
|
||||||
|
SERVICE_BINARY_NAMES,
|
||||||
|
assertExecutable,
|
||||||
|
serviceDataDir,
|
||||||
|
serviceConfigPath,
|
||||||
|
winwsPath
|
||||||
|
} from './paths'
|
||||||
import {
|
import {
|
||||||
applyServiceConfig,
|
applyServiceConfig,
|
||||||
queryService,
|
queryService,
|
||||||
@@ -28,18 +35,34 @@ async function readInstalledMarkerLine(): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sha256(path: string): Promise<string> {
|
||||||
|
return createHash('sha256')
|
||||||
|
.update(await readFile(path))
|
||||||
|
.digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Служба всегда запускает защищённую копию winws.exe (protectedWinwsPath), а не оригинал
|
* Служба запускает защищённую копию бинарников, а не оригинал из папки приложения — значит
|
||||||
* из папки установки приложения (winwsPath) — значит переустановку/обновление приложения
|
* обновление по самому пути службы не отследить. Сравниваем содержимое: время изменения тут
|
||||||
* по самому пути службы не отследить, как раньше. Вместо этого сравниваем размер и время
|
* не годится совсем, `scripts/fetch-zapret.mjs` кладёт файлы через `copyFile`, а тот ставит
|
||||||
* изменения оригинала с уже скопированным файлом: разошлись — обновление zapret вышло
|
* текущее время — после каждой сборки CI mtime новее, хотя версия zapret запинована и байты
|
||||||
* позже последней копии, нужно копировать заново (а значит и UAC).
|
* те же. По mtime мы на ровном месте требовали UAC при каждом обновлении приложения и лезли
|
||||||
|
* копировать поверх WinDivert64.sys, ещё загруженного в ядро.
|
||||||
*/
|
*/
|
||||||
async function binariesNeedCopy(): Promise<boolean> {
|
async function binariesNeedCopy(): Promise<boolean> {
|
||||||
|
const sourceDir = dirname(winwsPath())
|
||||||
|
const targetDir = serviceDataDir()
|
||||||
try {
|
try {
|
||||||
const [source, copy] = await Promise.all([stat(winwsPath()), stat(protectedWinwsPath())])
|
for (const name of SERVICE_BINARY_NAMES) {
|
||||||
return source.size !== copy.size || source.mtimeMs > copy.mtimeMs
|
const [source, copy] = await Promise.all([
|
||||||
|
sha256(join(sourceDir, name)),
|
||||||
|
sha256(join(targetDir, name))
|
||||||
|
])
|
||||||
|
if (source !== copy) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
} catch {
|
} catch {
|
||||||
|
// Какого-то файла ещё нет (первая установка) или он нечитаем — надёжнее скопировать.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,19 @@ export function serviceConfigPath(): string {
|
|||||||
return join(serviceDataDir(), 'strategy.cfg')
|
return join(serviceDataDir(), 'strategy.cfg')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Файлы, которые служба запускает из своего защищённого каталога: сам winws и всё, от чего
|
||||||
|
* он зависит при старте. Список нужен и `engine.win32.ts` (решить, надо ли копировать),
|
||||||
|
* и повышенному скрипту в `service.win32.ts` (собственно копирование) — держим его здесь,
|
||||||
|
* чтобы он не разъехался на две копии.
|
||||||
|
*/
|
||||||
|
export const SERVICE_BINARY_NAMES = [
|
||||||
|
'winws.exe',
|
||||||
|
'cygwin1.dll',
|
||||||
|
'WinDivert.dll',
|
||||||
|
'WinDivert64.sys'
|
||||||
|
] as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Защищённая копия `winws.exe` (+ cygwin1.dll, WinDivert.dll, WinDivert64.sys) — на неё,
|
* Защищённая копия `winws.exe` (+ cygwin1.dll, WinDivert.dll, WinDivert64.sys) — на неё,
|
||||||
* а не на оригинал из папки установки приложения, указывает служба. Установка теперь
|
* а не на оригинал из папки установки приложения, указывает служба. Установка теперь
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
isElevated,
|
isElevated,
|
||||||
runElevatedServiceTask
|
runElevatedServiceTask
|
||||||
} from './elevated-task.win32'
|
} from './elevated-task.win32'
|
||||||
import { serviceConfigPath, serviceDataDir, winwsPath } from './paths'
|
import { SERVICE_BINARY_NAMES, serviceConfigPath, serviceDataDir, winwsPath } from './paths'
|
||||||
|
|
||||||
const run = promisify(execFile)
|
const run = promisify(execFile)
|
||||||
|
|
||||||
@@ -31,6 +31,8 @@ const ERROR_SERVICE_ALREADY_RUNNING = 1056
|
|||||||
const ERROR_SERVICE_NOT_ACTIVE = 1062
|
const ERROR_SERVICE_NOT_ACTIVE = 1062
|
||||||
|
|
||||||
export type ServiceState = 'Running' | 'Stopped' | 'StartPending' | 'StopPending' | 'Other'
|
export type ServiceState = 'Running' | 'Stopped' | 'StartPending' | 'StopPending' | 'Other'
|
||||||
|
/** То, чего можно дождаться от службы. `Absent` — службы в системе нет (снята). */
|
||||||
|
type WaitState = 'Running' | 'Stopped' | 'Absent'
|
||||||
export type ServiceStartMode = 'Auto' | 'Manual' | 'Disabled' | 'Other'
|
export type ServiceStartMode = 'Auto' | 'Manual' | 'Disabled' | 'Other'
|
||||||
|
|
||||||
export interface ServiceInfo {
|
export interface ServiceInfo {
|
||||||
@@ -40,10 +42,6 @@ export interface ServiceInfo {
|
|||||||
binPath: string
|
binPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitCodeOf(error: unknown): number {
|
function exitCodeOf(error: unknown): number {
|
||||||
const code = (error as { code?: number | string } | undefined)?.code
|
const code = (error as { code?: number | string } | undefined)?.code
|
||||||
return typeof code === 'number' ? code : 1
|
return typeof code === 'number' ? code : 1
|
||||||
@@ -81,12 +79,29 @@ function normalizeStartMode(raw: string | undefined): ServiceStartMode {
|
|||||||
* Опрос через `Get-CimInstance Win32_Service`, а не парсинг вывода `sc query` — у CIM
|
* Опрос через `Get-CimInstance Win32_Service`, а не парсинг вывода `sc query` — у CIM
|
||||||
* `State`/`StartMode` это английские константы независимо от языка Windows, а `sc` печатает
|
* `State`/`StartMode` это английские константы независимо от языка Windows, а `sc` печатает
|
||||||
* локализованный текст, который парсить надёжно нельзя.
|
* локализованный текст, который парсить надёжно нельзя.
|
||||||
|
*
|
||||||
|
* Ждём тоже здесь, внутри одного скрипта: запуск powershell.exe стоит около секунды (он
|
||||||
|
* поднимает .NET), а опросов до перехода службы в нужное состояние бывает с десяток. Раньше
|
||||||
|
* каждый опрос был отдельным процессом, и включение обхода складывалось в основном из этих
|
||||||
|
* запусков; теперь на всё ожидание приходится один. Сам опрос внутри CIM — десятки мс.
|
||||||
*/
|
*/
|
||||||
export async function queryService(): Promise<ServiceInfo | null> {
|
function queryScript(until: readonly WaitState[], timeoutMs: number): string {
|
||||||
const script =
|
return `
|
||||||
`Get-CimInstance Win32_Service -Filter "Name='${SERVICE_NAME}'" | ` +
|
$targets = @(${until.map((state) => `'${state}'`).join(',')})
|
||||||
'Select-Object State,StartMode,PathName | ConvertTo-Json -Compress'
|
$deadline = [DateTime]::UtcNow.AddMilliseconds(${timeoutMs})
|
||||||
|
for (;;) {
|
||||||
|
$svc = Get-CimInstance Win32_Service -Filter "Name='${SERVICE_NAME}'" |
|
||||||
|
Select-Object State,StartMode,PathName
|
||||||
|
$state = if ($svc) { $svc.State } else { 'Absent' }
|
||||||
|
if ($targets.Count -eq 0 -or $targets -contains $state) { break }
|
||||||
|
if ([DateTime]::UtcNow -ge $deadline) { break }
|
||||||
|
Start-Sleep -Milliseconds ${POLL_INTERVAL_MS}
|
||||||
|
}
|
||||||
|
if ($svc) { $svc | ConvertTo-Json -Compress } else { 'null' }
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runQuery(script: string): Promise<ServiceInfo | null> {
|
||||||
let stdout: string
|
let stdout: string
|
||||||
try {
|
try {
|
||||||
;({ stdout } = await run('powershell.exe', ['-NoProfile', '-Command', script]))
|
;({ stdout } = await run('powershell.exe', ['-NoProfile', '-Command', script]))
|
||||||
@@ -108,6 +123,19 @@ export async function queryService(): Promise<ServiceInfo | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Текущее состояние службы; `null` — службы в системе нет. */
|
||||||
|
export function queryService(): Promise<ServiceInfo | null> {
|
||||||
|
return runQuery(queryScript([], 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ждёт одного из состояний и возвращает последнее увиденное — в том числе по таймауту,
|
||||||
|
* поэтому результат у вызывающего кода обязателен к проверке.
|
||||||
|
*/
|
||||||
|
function waitForService(...states: WaitState[]): Promise<ServiceInfo | null> {
|
||||||
|
return runQuery(queryScript(states, POLL_TIMEOUT_MS))
|
||||||
|
}
|
||||||
|
|
||||||
function explainScError(error: unknown, verb: string): string {
|
function explainScError(error: unknown, verb: string): string {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
if (/access is denied/i.test(message)) {
|
if (/access is denied/i.test(message)) {
|
||||||
@@ -119,18 +147,6 @@ function explainScError(error: unknown, verb: string): string {
|
|||||||
return `Не удалось ${verb} службу: ${message}`
|
return `Не удалось ${verb} службу: ${message}`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollUntil(
|
|
||||||
predicate: (info: ServiceInfo | null) => boolean
|
|
||||||
): Promise<ServiceInfo | null> {
|
|
||||||
const deadline = Date.now() + POLL_TIMEOUT_MS
|
|
||||||
for (;;) {
|
|
||||||
const info = await queryService()
|
|
||||||
if (predicate(info)) return info
|
|
||||||
if (Date.now() >= deadline) return info
|
|
||||||
await sleep(POLL_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `sc start` только просит SCM перевести службу в running и возвращается сразу — если не
|
* `sc start` только просит SCM перевести службу в running и возвращается сразу — если не
|
||||||
* дождаться реального состояния, свежепадающая служба (например, WinDivert не загрузился)
|
* дождаться реального состояния, свежепадающая служба (например, WinDivert не загрузился)
|
||||||
@@ -147,7 +163,7 @@ export async function startService(): Promise<ServiceInfo | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = await pollUntil((i) => i?.state === 'Running' || i?.state === 'Stopped')
|
const info = await waitForService('Running', 'Stopped')
|
||||||
if (info?.state !== 'Running') {
|
if (info?.state !== 'Running') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Служба запущена системой, но не перешла в рабочее состояние (текущее: ` +
|
`Служба запущена системой, но не перешла в рабочее состояние (текущее: ` +
|
||||||
@@ -169,12 +185,11 @@ export async function stopService(): Promise<ServiceInfo | null> {
|
|||||||
throw new Error(explainScError(error, 'остановить'), { cause: error })
|
throw new Error(explainScError(error, 'остановить'), { cause: error })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// `pollUntil` возвращает последнее состояние и по истечении таймаута, независимо от предиката,
|
// Результат обязательно проверяем: «остановка», не уложившаяся в POLL_TIMEOUT_MS, иначе
|
||||||
// поэтому результат обязательно проверяем. Иначе «остановка», не уложившаяся в POLL_TIMEOUT_MS,
|
// молча сошла бы за успешную — и вызывающий код (перенастройка стратегии в engine.win32.ts)
|
||||||
// молча сходила бы за успешную — и вызывающий код (перенастройка стратегии в engine.win32.ts)
|
|
||||||
// пошёл бы копировать winws.exe поверх ещё работающего, ровно в ту ошибку, от которой
|
// пошёл бы копировать winws.exe поверх ещё работающего, ровно в ту ошибку, от которой
|
||||||
// остановка и защищает.
|
// остановка и защищает.
|
||||||
const info = await pollUntil((i) => i === null || i.state === 'Stopped')
|
const info = await waitForService('Stopped', 'Absent')
|
||||||
if (info && info.state !== 'Stopped') {
|
if (info && info.state !== 'Stopped') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Служба не остановилась за ${POLL_TIMEOUT_MS / 1000} с (состояние: ${info.state}).`
|
`Служба не остановилась за ${POLL_TIMEOUT_MS / 1000} с (состояние: ${info.state}).`
|
||||||
@@ -270,39 +285,45 @@ function Invoke-ScRaw($rawArgs) {
|
|||||||
return [pscustomobject]@{ ExitCode = $proc.ExitCode; Output = ("$stdout $stderr").Trim() }
|
return [pscustomobject]@{ ExitCode = $proc.ExitCode; Output = ("$stdout $stderr").Trim() }
|
||||||
}
|
}
|
||||||
|
|
||||||
# WinDivert64.sys — файл драйвера, загруженного в ядро: пока драйвер не выгружен, файл занят,
|
# WinDivert64.sys — образ драйвера, загруженного в ядро: пока драйвер не выгружен, файл
|
||||||
# и Copy-Item поверх него падает с «Процесс не может получить доступ к файлу ... используется
|
# занят, и Copy-Item поверх него падает с «файл используется другим процессом». SCM
|
||||||
# другим процессом». SCM рапортует службе состояние Stopped, как только winws.exe отдал сигнал
|
# рапортует Stopped, как только winws.exe отдал сигнал, а выгрузка идёт уже после — при
|
||||||
# об остановке, — выгрузка драйвера происходит уже после этого, поэтому при быстром
|
# быстром переподключении попадаем ровно в этот зазор.
|
||||||
# переподключении (выключил и сразу включил, сменил стратегию) перенастройка попадала ровно
|
|
||||||
# в этот зазор.
|
|
||||||
#
|
#
|
||||||
# Отсюда две меры. Первая и главная: не копировать то, что не менялось, — при смене стратегии
|
# Отсюда три ступени. Сравнение по ХЕШУ, а не по времени изменения: fetch-zapret.mjs кладёт
|
||||||
# бинарники те же самые, и трогать занятый .sys незачем (Copy-Item сохраняет LastWriteTime
|
# бинарники через copyFile, тот ставит текущее время, и после каждой сборки CI mtime новее
|
||||||
# источника, так что длина + время записи однозначно отличают уже скопированный файл).
|
# при тех же байтах — по времени мы лезли копировать занятый .sys после каждого обновления
|
||||||
# Вторая — на случай, когда копировать действительно надо (обновление приложения принесло
|
# приложения. Дальше короткие повторы — вдруг драйвер вот-вот выгрузится. Последняя ступень:
|
||||||
# новый zapret): ждём выгрузки драйвера повторами, а не падаем на первой же попытке.
|
# уводим занятый файл в сторону переименованием (NTFS разрешает переименовать загруженный
|
||||||
|
# образ, хотя удалить его не даёт) и кладём новый на освободившееся имя.
|
||||||
function Copy-IfChanged($source, $destination) {
|
function Copy-IfChanged($source, $destination) {
|
||||||
$src = Get-Item -LiteralPath $source
|
|
||||||
if (Test-Path -LiteralPath $destination) {
|
if (Test-Path -LiteralPath $destination) {
|
||||||
$dst = Get-Item -LiteralPath $destination
|
$sourceHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $source).Hash
|
||||||
if ($dst.Length -eq $src.Length -and $dst.LastWriteTimeUtc -eq $src.LastWriteTimeUtc) { return }
|
$copyHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $destination).Hash
|
||||||
|
if ($sourceHash -eq $copyHash) { return }
|
||||||
}
|
}
|
||||||
|
|
||||||
$attempts = 20
|
for ($attempt = 1; $attempt -le 6; $attempt++) {
|
||||||
for ($attempt = 1; $attempt -le $attempts; $attempt++) {
|
|
||||||
try {
|
try {
|
||||||
Copy-Item -LiteralPath $source -Destination $destination -Force
|
Copy-Item -LiteralPath $source -Destination $destination -Force
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
if ($attempt -eq $attempts) {
|
|
||||||
throw "Не удалось обновить $destination за 10 с: $($_.Exception.Message) Если обход только что был выключен, драйвер WinDivert ещё выгружается — повторите через несколько секунд."
|
|
||||||
}
|
|
||||||
Start-Sleep -Milliseconds 500
|
Start-Sleep -Milliseconds 500
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stale = "$destination.old"
|
||||||
|
Remove-Item -LiteralPath $stale -Force -ErrorAction SilentlyContinue
|
||||||
|
Move-Item -LiteralPath $destination -Destination $stale -Force
|
||||||
|
Copy-Item -LiteralPath $source -Destination $destination -Force
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
throw "Не удалось обновить $destination : $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$payload = Get-Content -Raw -Path $PayloadPath | ConvertFrom-Json
|
$payload = Get-Content -Raw -Path $PayloadPath | ConvertFrom-Json
|
||||||
@@ -332,10 +353,15 @@ try {
|
|||||||
# cygwin1.dll/WinDivert.dll/WinDivert64.sys, от них он зависит при запуске.
|
# cygwin1.dll/WinDivert.dll/WinDivert64.sys, от них он зависит при запуске.
|
||||||
$sourceDir = Split-Path -Parent $payload.winwsPath
|
$sourceDir = Split-Path -Parent $payload.winwsPath
|
||||||
$protectedWinws = Join-Path $dir 'winws.exe'
|
$protectedWinws = Join-Path $dir 'winws.exe'
|
||||||
foreach ($name in @('winws.exe', 'cygwin1.dll', 'WinDivert.dll', 'WinDivert64.sys')) {
|
foreach ($name in $payload.binaryNames) {
|
||||||
Copy-IfChanged (Join-Path $sourceDir $name) (Join-Path $dir $name)
|
Copy-IfChanged (Join-Path $sourceDir $name) (Join-Path $dir $name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Хвосты от прошлой перенастройки: тогда файл был занят загруженным драйвером, сейчас
|
||||||
|
# он, скорее всего, уже свободен. Не вышло — не беда, попробуем в следующий раз.
|
||||||
|
Get-ChildItem -LiteralPath $dir -Filter '*.old' -ErrorAction SilentlyContinue |
|
||||||
|
Remove-Item -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# sc.exe читает binPath как ОДИН аргумент — значит всю строку нужно взять в одну пару
|
# sc.exe читает binPath как ОДИН аргумент — значит всю строку нужно взять в одну пару
|
||||||
# кавычек, а внутренние кавычки экранировать как \\" (без этого CommandLineToArgvW режет
|
# кавычек, а внутренние кавычки экранировать как \\" (без этого CommandLineToArgvW режет
|
||||||
# значение на два токена по пробелу перед "@", и sc create падает с кодом 1639).
|
# значение на два токена по пробелу перед "@", и sc create падает с кодом 1639).
|
||||||
@@ -500,6 +526,7 @@ export async function applyServiceConfig(options: ApplyServiceConfigOptions): Pr
|
|||||||
configPath: serviceConfigPath(),
|
configPath: serviceConfigPath(),
|
||||||
configBody: options.configBody,
|
configBody: options.configBody,
|
||||||
winwsPath: winwsPath(),
|
winwsPath: winwsPath(),
|
||||||
|
binaryNames: SERVICE_BINARY_NAMES,
|
||||||
autoStart: options.autoStart
|
autoStart: options.autoStart
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user