@lvce-editor/shared-process 0.115.22 → 0.115.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/parts/AutoUpdater/AutoUpdater.ipc.js +2 -0
- package/src/parts/BuiltinExtensionsPath/BuiltinExtensionsPath.js +1 -1
- package/src/parts/HandleElectronReady/HandleElectronReady.js +2 -0
- package/src/parts/ModuleMap/ModuleMap.js +1 -0
- package/src/parts/Platform/Platform.js +3 -3
- package/src/parts/PreloadUrl/PreloadUrl.js +1 -1
- package/src/parts/WindowsStagedUpdate/WindowsStagedUpdate.js +230 -0
- package/src/parts/WindowsUpdateScripts/WindowsUpdateScripts.js +123 -0
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as UpdateLog from '../UpdateLog/UpdateLog.js'
|
|
2
|
+
import * as WindowsStagedUpdate from '../WindowsStagedUpdate/WindowsStagedUpdate.js'
|
|
2
3
|
import * as AutoUpdater from './AutoUpdater.js'
|
|
3
4
|
|
|
4
5
|
export const Commands = {
|
|
6
|
+
'AutoUpdater.checkWindowsUpdate': WindowsStagedUpdate.check,
|
|
5
7
|
'AutoUpdater.getAutoUpdateType': AutoUpdater.getAutoUpdateType,
|
|
6
8
|
'AutoUpdater.getLatestVersion': AutoUpdater.getLatestVersion,
|
|
7
9
|
'AutoUpdater.getPlatform': AutoUpdater.getPlatform,
|
|
@@ -3,6 +3,6 @@ import { fileURLToPath } from 'url'
|
|
|
3
3
|
|
|
4
4
|
export const getBuiltinExtensionsPath = () => {
|
|
5
5
|
const staticServerPath = fileURLToPath(import.meta.resolve('@lvce-editor/static-server'))
|
|
6
|
-
const builtinExtensionsPath = join(staticServerPath, '..', '..', 'static', '
|
|
6
|
+
const builtinExtensionsPath = join(staticServerPath, '..', '..', 'static', '5c8581c', 'extensions')
|
|
7
7
|
return builtinExtensionsPath
|
|
8
8
|
}
|
|
@@ -7,6 +7,7 @@ import * as Preferences from '../Preferences/Preferences.js'
|
|
|
7
7
|
import * as PreloadUrl from '../PreloadUrl/PreloadUrl.js'
|
|
8
8
|
import * as Process from '../Process/Process.js'
|
|
9
9
|
import * as TransientLinkedExtensions from '../TransientLinkedExtensions/TransientLinkedExtensions.js'
|
|
10
|
+
import * as WindowsStagedUpdate from '../WindowsStagedUpdate/WindowsStagedUpdate.js'
|
|
10
11
|
|
|
11
12
|
export const handleElectronReady = async (parsedArgs, workingDirectory) => {
|
|
12
13
|
Assert.object(parsedArgs)
|
|
@@ -25,6 +26,7 @@ export const handleElectronReady = async (parsedArgs, workingDirectory) => {
|
|
|
25
26
|
const preferences = await Preferences.getAllSafe()
|
|
26
27
|
const preloadUrl = PreloadUrl.getPreloadUrl()
|
|
27
28
|
await AppWindow.createAppWindow({ parsedArgs, preferences, preloadUrl, workingDirectory })
|
|
29
|
+
await WindowsStagedUpdate.confirmStartup()
|
|
28
30
|
} catch (error) {
|
|
29
31
|
Logger.error(error)
|
|
30
32
|
Process.exit(ExitCode.ExpectedError)
|
|
@@ -7,6 +7,7 @@ export const getModuleId = (commandId) => {
|
|
|
7
7
|
return ModuleId.TextDocument
|
|
8
8
|
case 'AttachDebugger.attachDebugger':
|
|
9
9
|
return ModuleId.AttachDebugger
|
|
10
|
+
case 'AutoUpdater.checkWindowsUpdate':
|
|
10
11
|
case 'AutoUpdater.getAutoUpdateType':
|
|
11
12
|
case 'AutoUpdater.getLatestVersion':
|
|
12
13
|
case 'AutoUpdater.getPlatform':
|
|
@@ -56,11 +56,11 @@ export const getSetupName = () => {
|
|
|
56
56
|
return 'Lvce-Setup'
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export const version = '0.115.
|
|
59
|
+
export const version = '0.115.24'
|
|
60
60
|
|
|
61
|
-
export const commit = '
|
|
61
|
+
export const commit = '5c8581c'
|
|
62
62
|
|
|
63
|
-
export const date = '2026-09-
|
|
63
|
+
export const date = '2026-09-19T12:57:04.000Z'
|
|
64
64
|
|
|
65
65
|
export const getVersion = () => {
|
|
66
66
|
return version
|
|
@@ -2,5 +2,5 @@ import { join } from 'node:path'
|
|
|
2
2
|
import * as Root from '../Root/Root.js'
|
|
3
3
|
|
|
4
4
|
export const getPreloadUrl = () => {
|
|
5
|
-
return join(Root.root, 'static', '
|
|
5
|
+
return join(Root.root, 'static', '5c8581c', 'packages', 'preload', 'dist', 'index.js')
|
|
6
6
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process'
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
3
|
+
import { closeSync, createReadStream, createWriteStream } from 'node:fs'
|
|
4
|
+
import { access, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { basename, dirname, join } from 'node:path'
|
|
6
|
+
import { Readable } from 'node:stream'
|
|
7
|
+
import { pipeline } from 'node:stream/promises'
|
|
8
|
+
import { promisify } from 'node:util'
|
|
9
|
+
import * as CompareVersion from '../CompareVersion/CompareVersion.js'
|
|
10
|
+
import * as ElectronDialog from '../ElectronDialog/ElectronDialog.js'
|
|
11
|
+
import * as Exit from '../Exit/Exit.js'
|
|
12
|
+
import * as Platform from '../Platform/Platform.js'
|
|
13
|
+
import * as UpdateLog from '../UpdateLog/UpdateLog.js'
|
|
14
|
+
import * as WindowsUpdateScripts from '../WindowsUpdateScripts/WindowsUpdateScripts.js'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
let running = false
|
|
30
|
+
let prepared
|
|
31
|
+
const RE_VERSION = /^\d+\.\d+\.\d+$/
|
|
32
|
+
const RE_DIGEST = /^sha256:[a-f0-9]{64}$/
|
|
33
|
+
const RE_TAG = /^v/
|
|
34
|
+
const RE_TOKEN = /^[a-f0-9]{32}$/
|
|
35
|
+
|
|
36
|
+
const powershell = () => join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
|
|
37
|
+
const argumentsFor = (script) => [
|
|
38
|
+
'-NoProfile',
|
|
39
|
+
'-NonInteractive',
|
|
40
|
+
'-EncodedCommand',
|
|
41
|
+
Buffer.from(script, 'utf16le').toString('base64'),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
export const validateAsset = (asset, version, arch) => {
|
|
45
|
+
if (!RE_VERSION.test(version) || !['x64', 'arm64'].includes(arch)) {
|
|
46
|
+
throw new Error('Unsupported Windows update version or architecture')
|
|
47
|
+
}
|
|
48
|
+
const name = `Lvce-Update-v${version}-${arch}.zip`
|
|
49
|
+
if (asset.name !== name || asset.browser_download_url !== `https://github.com/lvce-editor/lvce-editor/releases/download/v${version}/${name}`) {
|
|
50
|
+
throw new Error('Unexpected Windows update asset')
|
|
51
|
+
}
|
|
52
|
+
if (!RE_DIGEST.test(asset.digest)) {
|
|
53
|
+
throw new Error('Windows update requires a published SHA256 digest')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const verifyDigest = async (path, expected) => {
|
|
58
|
+
const hash = createHash('sha256')
|
|
59
|
+
for await (const chunk of createReadStream(path)) {
|
|
60
|
+
hash.update(chunk)
|
|
61
|
+
}
|
|
62
|
+
if (`sha256:${hash.digest('hex')}` !== expected) {
|
|
63
|
+
throw new Error('Windows update checksum mismatch')
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const prepare = async (asset, version) => {
|
|
68
|
+
if (prepared?.plan.version === version) {
|
|
69
|
+
return prepared
|
|
70
|
+
}
|
|
71
|
+
validateAsset(asset, version, process.arch)
|
|
72
|
+
const install = dirname(process.execPath)
|
|
73
|
+
// Sibling directories guarantee same-volume renames. Never stage inside the
|
|
74
|
+
// live application: the old installer may remove that entire tree.
|
|
75
|
+
const token = randomBytes(16).toString('hex')
|
|
76
|
+
const stage = `${install}.stage-${token}`
|
|
77
|
+
const work = `${install}.updates`
|
|
78
|
+
await mkdir(work, { recursive: true })
|
|
79
|
+
const path = join(work, `${token}.json`)
|
|
80
|
+
const plan = {
|
|
81
|
+
archive: join(work, `${token}.zip`),
|
|
82
|
+
backup: `${install}.backup-${token}`,
|
|
83
|
+
exe: basename(process.execPath),
|
|
84
|
+
install,
|
|
85
|
+
log: UpdateLog.getPath(),
|
|
86
|
+
parentPid: process.ppid,
|
|
87
|
+
ready: join(work, `${token}.ready`),
|
|
88
|
+
stage,
|
|
89
|
+
token,
|
|
90
|
+
version,
|
|
91
|
+
}
|
|
92
|
+
await writeFile(path, JSON.stringify(plan), { flag: 'wx' })
|
|
93
|
+
UpdateLog.write(`Staged update download started version=${version}`)
|
|
94
|
+
const response = await fetch(asset.browser_download_url, { signal: AbortSignal.timeout(600_000) })
|
|
95
|
+
if (!response.ok || !response.body) {
|
|
96
|
+
throw new Error(`Update download failed: ${response.status}`)
|
|
97
|
+
}
|
|
98
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(plan.archive, { flags: 'wx' }))
|
|
99
|
+
await verifyDigest(plan.archive, asset.digest)
|
|
100
|
+
UpdateLog.write(`Staged update download verified version=${version}; extraction started`)
|
|
101
|
+
await promisify(execFile)(powershell(), argumentsFor(WindowsUpdateScripts.extract), {
|
|
102
|
+
env: { ...process.env, LVCE_UPDATE_PLAN: path },
|
|
103
|
+
timeout: 600_000,
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
})
|
|
106
|
+
const config = JSON.parse(await readFile(join(stage, 'resources', 'app', 'config.json'), 'utf8'))
|
|
107
|
+
if (config.version !== version) {
|
|
108
|
+
throw new Error('Extracted application version does not match release')
|
|
109
|
+
}
|
|
110
|
+
await access(join(stage, plan.exe))
|
|
111
|
+
// Retain the registered NSIS uninstaller for normal uninstall / repair.
|
|
112
|
+
// The update archive contains only application files, never user data.
|
|
113
|
+
await copyFile(join(install, `Uninstall ${Platform.productNameLong}.exe`), join(stage, `Uninstall ${Platform.productNameLong}.exe`))
|
|
114
|
+
await copyFile(join(install, 'uninstallerIcon.ico'), join(stage, 'uninstallerIcon.ico')).catch((error) => {
|
|
115
|
+
if (error.code !== 'ENOENT') {
|
|
116
|
+
throw error
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
await writeFile(join(stage, '.lvce-update.json'), JSON.stringify({ token, version }))
|
|
120
|
+
UpdateLog.write(`Staged update ready version=${version} stage=${stage}`)
|
|
121
|
+
prepared = { path, plan }
|
|
122
|
+
return prepared
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const restart = async ({ path, plan } ) => {
|
|
126
|
+
await writeFile(path, JSON.stringify({ ...plan, parentPid: process.ppid }))
|
|
127
|
+
await rm(`${path}.started`, { force: true })
|
|
128
|
+
await writeFile(`${path}.helper.ps1`, WindowsUpdateScripts.activate)
|
|
129
|
+
const output = UpdateLog.openOutput()
|
|
130
|
+
const child = spawn(powershell(), argumentsFor(WindowsUpdateScripts.activate), {
|
|
131
|
+
cwd: dirname(plan.install),
|
|
132
|
+
detached: true,
|
|
133
|
+
env: { ...process.env, LVCE_UPDATE_PLAN: path },
|
|
134
|
+
stdio: ['ignore', output, output],
|
|
135
|
+
windowsHide: true,
|
|
136
|
+
})
|
|
137
|
+
closeSync(output)
|
|
138
|
+
await new Promise ((resolve, reject) => {
|
|
139
|
+
child.once('spawn', resolve)
|
|
140
|
+
child.once('error', reject)
|
|
141
|
+
})
|
|
142
|
+
child.unref()
|
|
143
|
+
// Do not exit if PowerShell is blocked or the helper cannot initialize.
|
|
144
|
+
for (let attempt = 0; attempt < 300; attempt++) {
|
|
145
|
+
if ((await readFile(`${path}.started`, 'utf8').catch(() => '')) === 'ready') {
|
|
146
|
+
UpdateLog.write('Staged update helper acknowledged; exiting editor')
|
|
147
|
+
await Exit.exit()
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (child.exitCode !== null) {
|
|
151
|
+
throw new Error(`Update helper exited before startup (${child.exitCode})`)
|
|
152
|
+
}
|
|
153
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
154
|
+
}
|
|
155
|
+
child.kill()
|
|
156
|
+
throw new Error('Update helper did not acknowledge startup; editor left running')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Returns false only when the release predates staged-update artifacts, allowing
|
|
160
|
+
// the caller to retain the existing NSIS update path for those releases.
|
|
161
|
+
export const check = async (silent, windowId) => {
|
|
162
|
+
if (running) {
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
running = true
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch('https://api.github.com/repos/lvce-editor/lvce-editor/releases/latest', { signal: AbortSignal.timeout(30_000) })
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
throw new Error(`Release check failed: ${response.status}`)
|
|
170
|
+
}
|
|
171
|
+
const release = await response.json()
|
|
172
|
+
const version = release.tag_name.replace(RE_TAG, '')
|
|
173
|
+
if (!CompareVersion.isGreater(version, Platform.version)) {
|
|
174
|
+
if (!silent) {
|
|
175
|
+
await ElectronDialog.showMessageBox({ buttons: ['OK'], message: 'No update available.', type: 'info', windowId })
|
|
176
|
+
}
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
const asset = release.assets.find((item) => item.name === `Lvce-Update-v${version}-${process.arch}.zip`)
|
|
180
|
+
if (!asset) {
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
if (!silent) {
|
|
184
|
+
const answer = await ElectronDialog.showMessageBox({
|
|
185
|
+
buttons: ['Prepare Update', 'Cancel'],
|
|
186
|
+
defaultId: 0,
|
|
187
|
+
message: `Download and prepare update ${version}?`,
|
|
188
|
+
type: 'question',
|
|
189
|
+
windowId,
|
|
190
|
+
})
|
|
191
|
+
if (answer !== 0) {
|
|
192
|
+
return true
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const update = await prepare(asset, version)
|
|
196
|
+
const answer = await ElectronDialog.showMessageBox({
|
|
197
|
+
buttons: ['Restart', 'Later'],
|
|
198
|
+
defaultId: 0,
|
|
199
|
+
message: `Update ${version} is ready. Restart now?`,
|
|
200
|
+
type: 'question',
|
|
201
|
+
windowId,
|
|
202
|
+
})
|
|
203
|
+
if (answer === 0) {
|
|
204
|
+
await restart(update)
|
|
205
|
+
}
|
|
206
|
+
return true
|
|
207
|
+
} catch (error) {
|
|
208
|
+
UpdateLog.writeSafe(`Staged update failed: ${error.stack || error.message}`)
|
|
209
|
+
throw error
|
|
210
|
+
} finally {
|
|
211
|
+
running = false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const confirmStartup = async () => {
|
|
216
|
+
if (!Platform.isWindows || !Platform.isProduction) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
const install = dirname(process.execPath)
|
|
220
|
+
const marker = await readFile(join(install, '.lvce-update.json'), 'utf8').catch(() => '')
|
|
221
|
+
if (!marker) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
const { token, version } = JSON.parse(marker)
|
|
225
|
+
if (!RE_TOKEN.test(token) || version !== Platform.version) {
|
|
226
|
+
throw new Error('Invalid staged update startup marker')
|
|
227
|
+
}
|
|
228
|
+
await writeFile(join(`${install}.updates`, `${token}.ready`), token)
|
|
229
|
+
UpdateLog.write(`Staged update startup confirmed version=${version}`)
|
|
230
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Executed by Windows PowerShell outside the directory being replaced. All
|
|
2
|
+
// variable input is read from a JSON plan, never interpolated into PowerShell.
|
|
3
|
+
export const activate = String.raw`
|
|
4
|
+
$ErrorActionPreference = 'Stop'
|
|
5
|
+
$plan = Get-Content -LiteralPath $env:LVCE_UPDATE_PLAN -Raw | ConvertFrom-Json
|
|
6
|
+
$install = [IO.Path]::GetFullPath($plan.install)
|
|
7
|
+
$stage = [IO.Path]::GetFullPath($plan.stage)
|
|
8
|
+
$backup = [IO.Path]::GetFullPath($plan.backup)
|
|
9
|
+
$parent = Split-Path -Parent $install
|
|
10
|
+
if ((Split-Path -Parent $stage) -ne $parent -or (Split-Path -Parent $backup) -ne $parent) { throw 'Update folders must be siblings' }
|
|
11
|
+
if ($stage -eq $install -or $backup -eq $install -or $stage -eq $backup) { throw 'Update folders must be distinct' }
|
|
12
|
+
if ((Split-Path -Leaf $stage) -ne ((Split-Path -Leaf $install) + '.stage-' + $plan.token)) { throw 'Invalid stage path' }
|
|
13
|
+
if ((Split-Path -Leaf $backup) -ne ((Split-Path -Leaf $install) + '.backup-' + $plan.token)) { throw 'Invalid backup path' }
|
|
14
|
+
if ($plan.token -notmatch '^[a-f0-9]{32}$') { throw 'Invalid update token' }
|
|
15
|
+
Set-Location -LiteralPath $parent
|
|
16
|
+
function Log($message) {
|
|
17
|
+
$line = [DateTime]::UtcNow.ToString('o') + ' [windows-swap] ' + $message + [Environment]::NewLine
|
|
18
|
+
[IO.File]::AppendAllText($plan.log, $line)
|
|
19
|
+
}
|
|
20
|
+
function Journal($phase) {
|
|
21
|
+
[IO.File]::WriteAllText($env:LVCE_UPDATE_PLAN + '.state', $phase)
|
|
22
|
+
Log $phase
|
|
23
|
+
}
|
|
24
|
+
function MoveFolder($from, $to) {
|
|
25
|
+
for ($attempt=0; $attempt -lt 50; $attempt++) {
|
|
26
|
+
try { [IO.Directory]::Move($from, $to); return } catch {
|
|
27
|
+
if ($attempt -eq 49) { throw }
|
|
28
|
+
Start-Sleep -Milliseconds 100
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
$movedOld = $false
|
|
33
|
+
$movedNew = $false
|
|
34
|
+
$editorExited = $false
|
|
35
|
+
$newProcess = $null
|
|
36
|
+
try {
|
|
37
|
+
Journal 'helper-ready'
|
|
38
|
+
[IO.File]::WriteAllText($env:LVCE_UPDATE_PLAN + '.started', 'ready')
|
|
39
|
+
# The application exits only after the helper acknowledges startup. Never
|
|
40
|
+
# kill an editor that refuses to exit, for example with an unsaved document.
|
|
41
|
+
$deadline = [DateTime]::UtcNow.AddSeconds(60)
|
|
42
|
+
while (Get-Process -Id $plan.parentPid -ErrorAction SilentlyContinue) {
|
|
43
|
+
if ([DateTime]::UtcNow -gt $deadline) { throw 'Editor did not exit; installation left intact' }
|
|
44
|
+
Start-Sleep -Milliseconds 100
|
|
45
|
+
}
|
|
46
|
+
Journal 'editor-exited'
|
|
47
|
+
$editorExited = $true
|
|
48
|
+
# Rerunning the saved helper after interruption restores a missing install.
|
|
49
|
+
if (!(Test-Path -LiteralPath $install) -and (Test-Path -LiteralPath $backup)) {
|
|
50
|
+
MoveFolder $backup $install
|
|
51
|
+
throw 'Recovered previous installation; retry the update'
|
|
52
|
+
}
|
|
53
|
+
if (Test-Path -LiteralPath $backup) { throw 'Backup already exists; recovery required' }
|
|
54
|
+
$config = Get-Content -LiteralPath (Join-Path $stage 'resources/app/config.json') -Raw | ConvertFrom-Json
|
|
55
|
+
if ($config.version -ne $plan.version) { throw 'Staged version does not match update plan' }
|
|
56
|
+
Journal 'moving-old'
|
|
57
|
+
MoveFolder $install $backup
|
|
58
|
+
$movedOld = $true
|
|
59
|
+
Journal 'moving-new'
|
|
60
|
+
MoveFolder $stage $install
|
|
61
|
+
$movedNew = $true
|
|
62
|
+
Journal 'launching-new'
|
|
63
|
+
$newProcess = Start-Process -FilePath (Join-Path $install $plan.exe) -ArgumentList '--updated' -WorkingDirectory $install -PassThru -WindowStyle Hidden
|
|
64
|
+
$deadline = [DateTime]::UtcNow.AddSeconds(60)
|
|
65
|
+
while (!(Test-Path -LiteralPath $plan.ready)) {
|
|
66
|
+
$newProcess.Refresh()
|
|
67
|
+
if ($newProcess.HasExited) { throw 'Updated application exited before confirming startup' }
|
|
68
|
+
if ([DateTime]::UtcNow -gt $deadline) { throw 'Updated application did not confirm startup' }
|
|
69
|
+
Start-Sleep -Milliseconds 100
|
|
70
|
+
}
|
|
71
|
+
if ((Get-Content -LiteralPath $plan.ready -Raw) -ne $plan.token) { throw 'Invalid startup acknowledgment' }
|
|
72
|
+
Journal 'startup-confirmed'
|
|
73
|
+
try {
|
|
74
|
+
Get-ChildItem 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall' | ForEach-Object {
|
|
75
|
+
$entry = Get-ItemProperty -LiteralPath $_.PSPath
|
|
76
|
+
if ($entry.InstallLocation -and $entry.InstallLocation.TrimEnd('\') -eq $install.TrimEnd('\')) {
|
|
77
|
+
Set-ItemProperty -LiteralPath $_.PSPath -Name DisplayVersion -Value $plan.version
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch { Log ('Could not refresh installed version metadata: ' + $_.Exception.Message) }
|
|
81
|
+
# Keep rollback files until after startup, outside the user-visible outage.
|
|
82
|
+
Start-Sleep -Seconds 5
|
|
83
|
+
$newProcess.Refresh()
|
|
84
|
+
if ($newProcess.HasExited) { throw 'Updated application exited during startup' }
|
|
85
|
+
Journal 'cleanup-started'
|
|
86
|
+
try {
|
|
87
|
+
Remove-Item -LiteralPath $backup -Recurse -Force
|
|
88
|
+
if ($plan.archive -eq (Join-Path ($install + '.updates') ($plan.token + '.zip'))) {
|
|
89
|
+
Remove-Item -LiteralPath $plan.archive -Force -ErrorAction SilentlyContinue
|
|
90
|
+
}
|
|
91
|
+
Journal 'complete'
|
|
92
|
+
}
|
|
93
|
+
catch { Log ('Cleanup deferred; backup retained: ' + $_.Exception.Message) }
|
|
94
|
+
} catch {
|
|
95
|
+
Log ('Update failed: ' + $_.Exception.Message)
|
|
96
|
+
if ($movedOld) {
|
|
97
|
+
if ($newProcess -and !$newProcess.HasExited) {
|
|
98
|
+
& taskkill.exe /PID $newProcess.Id /T /F | Out-Null
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
if ($movedNew) { MoveFolder $install $stage }
|
|
102
|
+
MoveFolder $backup $install
|
|
103
|
+
Journal 'rolled-back'
|
|
104
|
+
Start-Process -FilePath (Join-Path $install $plan.exe) -WorkingDirectory $install -WindowStyle Hidden
|
|
105
|
+
} catch { Log ('Recovery required; previous installation preserved at ' + $backup + ': ' + $_.Exception.Message) }
|
|
106
|
+
} elseif ($editorExited -and (Test-Path -LiteralPath (Join-Path $install $plan.exe))) {
|
|
107
|
+
# A locked old folder or rejected plan must not leave the user without the
|
|
108
|
+
# still-intact editor after the requested shutdown.
|
|
109
|
+
try { Start-Process -FilePath (Join-Path $install $plan.exe) -WorkingDirectory $install -WindowStyle Hidden }
|
|
110
|
+
catch { Log ('Could not relaunch previous installation: ' + $_.Exception.Message) }
|
|
111
|
+
}
|
|
112
|
+
exit 1
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
|
|
116
|
+
export const extract = String.raw`
|
|
117
|
+
$ErrorActionPreference = 'Stop'
|
|
118
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
119
|
+
$plan = Get-Content -LiteralPath $env:LVCE_UPDATE_PLAN -Raw | ConvertFrom-Json
|
|
120
|
+
# ExtractToDirectory rejects entries outside the destination and does not
|
|
121
|
+
# overwrite existing files. Preparation never modifies the running app.
|
|
122
|
+
[IO.Compression.ZipFile]::ExtractToDirectory($plan.archive, $plan.stage)
|
|
123
|
+
`
|