Skip to content

Commit 3bbff39

Browse files
authored
[FEAT] Improve and Fix Auto-Update on Windows and macOS (#1217)
* [FEAT] Add retry for auto updates * chore: comments * test only: revert after test * chore: lowered version for testing * i18n: updated error message * feat: improve update logic and error messages * feat: increase maximum update attempts and improve error handling logic * feat: For test only: disable update code signature verification on windows * chore: move updater to its own folder and create utils file * fix: pr comments and test settings * feat: add tracking for client update download start event * fix: remove redundant name field from electron-builder configuration --------- Co-authored-by: Flavio F Lima <flavioislima@users.noreply.github.com>
1 parent fbd24d3 commit 3bbff39

File tree

5 files changed

+235
-28
lines changed

5 files changed

+235
-28
lines changed

public/locales/en/translation.json

+19-2
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,25 @@
8888
"title": "Ubisoft Connect"
8989
},
9090
"update": {
91-
"message": "Something went wrong with the update! Please manually uninstall and reinstall HyperPlay.",
92-
"title": "Update Error"
91+
"addressUnreachable": "Address unreachable. Please check your internet connection.",
92+
"body": "Something went wrong with the update after multiple attempts! Please check the error message below or reinstall HyperPlay. error: {{error}}",
93+
"certAuthorityInvalid": "Certificate authority invalid. Please check your system time and date or open a ticket with HyperPlay support.",
94+
"certDateInvalid": "Certificate date invalid. Please check your system time and date or open a ticket with HyperPlay support.",
95+
"connectionAborted": "Connection aborted. Please check your internet connection.",
96+
"connectionClosed": "Connection closed. Please check your internet connection.",
97+
"connectionRefused": "Connection refused. Please check your internet connection.",
98+
"connectionReset": "Connection reset. Please check your internet connection.",
99+
"connectionTimedOut": "Connection timed out. Please check your internet connection.",
100+
"emptyResponse": "Empty response. Please check your internet connection.",
101+
"failed": "Download Failed. Please check your internet connection.",
102+
"http2ServerRefusedStream": "HTTP2 server refused stream. Please check your internet connection.",
103+
"internetDisconnected": "Internet disconnected. Please check your internet connection.",
104+
"message": "Error Updating",
105+
"nameNotResolved": "Name not resolved. Please check your internet connection.",
106+
"networkAccessDenied": "Network access denied. Please check your internet connection.",
107+
"networkChanged": "Network changed. Please check your internet connection.",
108+
"proxyConnectionFailed": "Proxy connection failed. Please check your proxy settings.",
109+
"sslProtocolError": "SSL protocol error. Please check your system time and date or open a ticket with HyperPlay support."
93110
},
94111
"winetricks": {
95112
"message": "Winetricks returned the following error during execution:{{newLine}}{{error}}",

src/backend/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ import {
151151
checkWineBeforeLaunch,
152152
runWineCommandOnGame
153153
} from './utils/compatibility_layers'
154-
import { isClientUpdating } from 'backend/updater'
154+
import { isClientUpdating } from 'backend/updater/updater'
155155

156156
/*
157157
* INSERT OTHER IPC HANDLERS HERE

src/backend/metrics/types.ts

+10
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,15 @@ export interface ClientUpdateNotified {
399399
sensitiveProperties?: never
400400
}
401401

402+
export interface ClientUpdateDownloading {
403+
event: 'Downloading Client Update'
404+
properties: {
405+
currentVersion: string
406+
newVersion: string
407+
}
408+
sensitiveProperties?: never
409+
}
410+
402411
export interface ClientUpdateError {
403412
event: 'Client Update Error'
404413
properties: {
@@ -460,6 +469,7 @@ export type PossibleMetricPayloads =
460469
| RewardClaimSuccess
461470
| RewardClaimError
462471
| ClientUpdateNotified
472+
| ClientUpdateDownloading
463473
| ClientUpdateError
464474
| ClientUpdateDownloaded
465475
| PatchingStarted

src/backend/updater.ts src/backend/updater/updater.ts

+83-25
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { dialog, shell } from 'electron'
1+
import { app, dialog, shell } from 'electron'
22
import { autoUpdater } from 'electron-updater'
33
import { t } from 'i18next'
44

5-
import { configStore, icon, isLinux } from './constants'
6-
import { logError, logInfo, LogPrefix } from './logger/logger'
7-
import { isOnline } from './online_monitor'
5+
import { configStore, icon, isLinux } from '../constants'
6+
import { logError, logInfo, LogPrefix } from '../logger/logger'
87
import { captureException } from '@sentry/electron'
9-
import { getFileSize } from './utils'
8+
import { getFileSize } from '../utils'
109
import { ClientUpdateStatuses } from '@hyperplay/utils'
11-
import { trackEvent } from './metrics/metrics'
12-
// to test auto update on windows locally make sure you added the option "verifyUpdateCodeSignature": false
13-
// under build.win in package.json and also change the app version to an old one there
10+
import { trackEvent } from '../metrics/metrics'
11+
import { getErrorMessage, removeCachedUpdatesFolder } from './utils'
12+
// to test auto update on windows locally make sure you added the option verifyUpdateCodeSignature: false
13+
// under build.win in electron-builder.yml and also change the app version to an old one there
1414

1515
const appSettings = configStore.get_nodefault('settings')
1616
const shouldCheckForUpdates = appSettings?.checkForUpdatesOnStartup === true
@@ -21,17 +21,35 @@ autoUpdater.autoInstallOnAppQuit = true
2121

2222
let isAppUpdating = false
2323
let hasUpdated = false
24+
let hasReportedDownloadStart = false
2425

25-
// check for updates every hour
26-
const checkUpdateInterval = 1 * 60 * 60 * 1000
27-
setInterval(() => {
28-
if (isOnline() && shouldCheckForUpdates) {
29-
autoUpdater.checkForUpdates()
26+
let updateAttempts = 0
27+
const MAX_UPDATE_ATTEMPTS = 10
28+
// check for updates every 3 hours
29+
const checkUpdateInterval = 3 * 1000 * 60 * 60
30+
31+
setInterval(async () => {
32+
if (shouldCheckForUpdates && !hasUpdated && !isAppUpdating) {
33+
logInfo('Checking for client updates...', LogPrefix.AutoUpdater)
34+
await autoUpdater.checkForUpdates()
3035
}
3136
}, checkUpdateInterval)
3237

3338
autoUpdater.on('update-available', async (info) => {
34-
if (!isOnline() || !shouldCheckForUpdates) {
39+
if (isAppUpdating && hasUpdated) {
40+
logInfo(
41+
'New update available, but user has already updated the app',
42+
LogPrefix.AutoUpdater
43+
)
44+
return
45+
}
46+
47+
if (!shouldCheckForUpdates) {
48+
logInfo(
49+
'New update available, but user has disabled auto updates',
50+
LogPrefix.AutoUpdater
51+
)
52+
3553
return
3654
}
3755
newVersion = info.version
@@ -56,6 +74,19 @@ autoUpdater.on('update-available', async (info) => {
5674
// log download progress
5775
autoUpdater.on('download-progress', (progress) => {
5876
isAppUpdating = true
77+
78+
// Track download start only once
79+
if (!hasReportedDownloadStart) {
80+
trackEvent({
81+
event: 'Downloading Client Update',
82+
properties: {
83+
currentVersion: autoUpdater.currentVersion.version,
84+
newVersion
85+
}
86+
})
87+
hasReportedDownloadStart = true
88+
}
89+
5990
logInfo(
6091
'Downloading HyperPlay update...' +
6192
`Download speed: ${progress.bytesPerSecond}, ` +
@@ -68,7 +99,10 @@ autoUpdater.on('download-progress', (progress) => {
6899
})
69100

70101
autoUpdater.on('update-downloaded', async () => {
71-
logInfo('App update is downloaded')
102+
logInfo('The App update was downloaded', LogPrefix.AutoUpdater)
103+
hasUpdated = true
104+
isAppUpdating = false
105+
hasReportedDownloadStart = false // Reset for potential future updates
72106

73107
trackEvent({
74108
event: 'Client Update Downloaded',
@@ -91,16 +125,38 @@ autoUpdater.on('update-downloaded', async () => {
91125
if (response === 1) {
92126
return autoUpdater.quitAndInstall()
93127
}
94-
hasUpdated = true
128+
logInfo('User chose not to update the app for now.', LogPrefix.AutoUpdater)
95129
})
96130

97131
autoUpdater.on('error', async (error) => {
98-
if (!isOnline()) {
132+
isAppUpdating = false
133+
const isNewVersion = newVersion !== app.getVersion()
134+
135+
// To avoid false positives, we should not show the error dialog if the app has already updated successfully
136+
if (hasUpdated || !isNewVersion) {
99137
return
100138
}
101139

102-
isAppUpdating = false
103-
logError(`Error updating HyperPlay: ${error.message}`, LogPrefix.AutoUpdater)
140+
const errorMessage = getErrorMessage(error.message)
141+
logError(`Error updating HyperPlay: ${errorMessage}`, LogPrefix.AutoUpdater)
142+
143+
// will remove cached updates when it fails to avoid corrupted updates
144+
if (updateAttempts > 3) {
145+
await removeCachedUpdatesFolder()
146+
}
147+
148+
updateAttempts++
149+
150+
if (updateAttempts < MAX_UPDATE_ATTEMPTS) {
151+
logInfo(
152+
`Retrying update attempt ${updateAttempts + 1}/${MAX_UPDATE_ATTEMPTS}`,
153+
LogPrefix.AutoUpdater
154+
)
155+
setTimeout(() => {
156+
autoUpdater.checkForUpdates()
157+
}, 6000)
158+
return
159+
}
104160

105161
trackEvent({
106162
event: 'Client Update Error',
@@ -115,17 +171,19 @@ autoUpdater.on('error', async (error) => {
115171
tags: {
116172
event: 'Client Update Error',
117173
currentVersion: autoUpdater.currentVersion.version,
118-
newVersion
174+
newVersion,
175+
totalAttempts: MAX_UPDATE_ATTEMPTS
119176
}
120177
})
121178

179+
updateAttempts = 0
180+
122181
const { response } = await dialog.showMessageBox({
123-
title: t('box.error.update.title', 'Error Updating'),
182+
title: t('box.error.update.message', 'Error Updating'),
124183
message: t(
125-
'box.error.update.message',
126-
`Something went wrong with the update after multiple attempts! Please manually uninstall and reinstall HyperPlay. error: ${JSON.stringify(
127-
error
128-
)}`
184+
'box.error.update.body',
185+
`Something went wrong with the update after multiple attempts! Please check the error message below or reinstall HyperPlay. error: {{error}}`,
186+
{ error: errorMessage }
129187
),
130188
type: 'error',
131189
buttons: [t('button.cancel', 'Cancel'), t('button.download', 'Download')]

src/backend/updater/utils.ts

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { isMac } from 'backend/constants'
2+
import { logError, LogPrefix } from 'backend/logger/logger'
3+
import { rm } from 'fs/promises'
4+
import { t } from 'i18next'
5+
import { homedir } from 'os'
6+
import { join } from 'path'
7+
8+
export async function removeCachedUpdatesFolder() {
9+
// remove hyperplay-updates folder from cache directory
10+
// on macOS: /Users/<username>/Library/Caches/hyperplay-updates
11+
// on Windows: C:\Users\<username>\AppData\Local\hyperplay-updates
12+
const macOSPath = join(homedir(), 'Library', 'Caches', 'hyperplay-updates')
13+
const windowsPath = join(homedir(), 'AppData', 'Local', 'hyperplay-updates')
14+
15+
try {
16+
await rm(isMac ? macOSPath : windowsPath, { recursive: true })
17+
} catch (error) {
18+
logError(
19+
`Error removing cached updates folder: ${error}`,
20+
LogPrefix.AutoUpdater
21+
)
22+
}
23+
}
24+
25+
const commonDownloadErrors: Record<string, () => string> = {
26+
ERR_NETWORK_CHANGED: () =>
27+
t(
28+
'box.error.update.networkChanged',
29+
'Network changed. Please check your internet connection.'
30+
),
31+
ERR_INTERNET_DISCONNECTED: () =>
32+
t(
33+
'box.error.update.internetDisconnected',
34+
'Internet disconnected. Please check your internet connection.'
35+
),
36+
ERR_CONNECTION_RESET: () =>
37+
t(
38+
'box.error.update.connectionReset',
39+
'Connection reset. Please check your internet connection.'
40+
),
41+
ERR_CONNECTION_CLOSED: () =>
42+
t(
43+
'box.error.update.connectionClosed',
44+
'Connection closed. Please check your internet connection.'
45+
),
46+
ERR_CONNECTION_TIMED_OUT: () =>
47+
t(
48+
'box.error.update.connectionTimedOut',
49+
'Connection timed out. Please check your internet connection.'
50+
),
51+
ERR_NAME_NOT_RESOLVED: () =>
52+
t(
53+
'box.error.update.nameNotResolved',
54+
'Name not resolved. Please check your internet connection.'
55+
),
56+
ERR_CONNECTION_REFUSED: () =>
57+
t(
58+
'box.error.update.connectionRefused',
59+
'Connection refused. Please check your internet connection.'
60+
),
61+
ERR_SSL_PROTOCOL_ERROR: () =>
62+
t(
63+
'box.error.update.sslProtocolError',
64+
'SSL protocol error. Please check your system time and date or open a ticket with HyperPlay support.'
65+
),
66+
ERR_CERT_AUTHORITY_INVALID: () =>
67+
t(
68+
'box.error.update.certAuthorityInvalid',
69+
'Certificate authority invalid. Please check your system time and date or open a ticket with HyperPlay support.'
70+
),
71+
ERR_NETWORK_ACCESS_DENIED: () =>
72+
t(
73+
'box.error.update.networkAccessDenied',
74+
'Network access denied. Please check your internet connection.'
75+
),
76+
ERR_PROXY_CONNECTION_FAILED: () =>
77+
t(
78+
'box.error.update.proxyConnectionFailed',
79+
'Proxy connection failed. Please check your proxy settings.'
80+
),
81+
ERR_CONNECTION_ABORTED: () =>
82+
t(
83+
'box.error.update.connectionAborted',
84+
'Connection aborted. Please check your internet connection.'
85+
),
86+
ERR_ADDRESS_UNREACHABLE: () =>
87+
t(
88+
'box.error.update.addressUnreachable',
89+
'Address unreachable. Please check your internet connection.'
90+
),
91+
ERR_CERT_DATE_INVALID: () =>
92+
t(
93+
'box.error.update.certDateInvalid',
94+
'Certificate date invalid. Please check your system time and date or open a ticket with HyperPlay support.'
95+
),
96+
ERR_HTTP2_SERVER_REFUSED_STREAM: () =>
97+
t(
98+
'box.error.update.http2ServerRefusedStream',
99+
'HTTP2 server refused stream. Please check your internet connection.'
100+
),
101+
ERR_EMPTY_RESPONSE: () =>
102+
t(
103+
'box.error.update.emptyResponse',
104+
'Empty response. Please check your internet connection.'
105+
),
106+
ERR_FAILED: () =>
107+
t(
108+
'box.error.update.failed',
109+
'Download Failed. Please check your internet connection.'
110+
)
111+
}
112+
113+
export function getErrorMessage(error: string): string {
114+
const trimmedError = error.replace('net::', '').trim()
115+
if (
116+
Object.prototype.hasOwnProperty.call(commonDownloadErrors, trimmedError)
117+
) {
118+
return commonDownloadErrors[trimmedError]()
119+
} else {
120+
return error
121+
}
122+
}

0 commit comments

Comments
 (0)