Skip to content

Commit d2b03d5

Browse files
[Fix] Installation of HP games, statuses, filters, stuck download and more (#328)
* fix: unzip, disk write and throttle progress * fix: progress on download screen * chore: mac notarize var env * feat: implement getExtraInfo for HP * fix: hide non-working items from submenu * feat: improve initial download and logging * chore: remove unused warning * feat: show extracting message on gamepage * fix: default platform on install dialog * fix: logging and button label * fix: OS filters for HP games * fix: downloaded bytes * fix: platform name and install size on gamepage * chore: log and mac build * add unit tests, fix extractZip * prettier --------- Co-authored-by: BrettCleary <27568879+BrettCleary@users.noreply.github.com>
1 parent 5354123 commit d2b03d5

File tree

16 files changed

+556
-155
lines changed

16 files changed

+556
-155
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@
161161
"@types/jsdom": "^20.0.0",
162162
"@types/qrcode": "^1.5.0",
163163
"@types/react-blockies": "^1.4.1",
164+
"@types/unzipper": "^0.10.6",
164165
"@walletconnect/web3-provider": "^1.8.0",
165166
"axios": "^0.26.1",
166167
"bn.js": "^5.2.1",
@@ -205,6 +206,7 @@
205206
"systeminformation": "^5.15.0",
206207
"ts-prune": "^0.10.3",
207208
"tslib": "^2.4.0",
209+
"unzipper": "^0.10.14",
208210
"web3": "^1.7.5"
209211
},
210212
"scripts": {

sign/notarize.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { notarize } = require('@electron/notarize')
33

44
exports.default = async function notarizing(context) {
55
const { electronPlatformName, appOutDir } = context
6-
if (electronPlatformName !== 'darwin' || process.env.NOTARIZE === 'false') {
6+
if (electronPlatformName !== 'darwin' || process.env.CSC_IDENTITY_AUTO_DISCOVERY === 'false') {
77
console.log('Notarizing skipped')
88
return
99
}

src/backend/__mocks__/test.zip

136 Bytes
Binary file not shown.

src/backend/__tests__/utils.test.ts

+155
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import { app } from 'electron'
33
import { logError } from '../logger/logger'
44
import * as utils from '../utils'
55
import { test_data } from './test_data/github-api-heroic-test-data.json'
6+
import path from 'path'
7+
import {
8+
copyFileSync,
9+
existsSync,
10+
readFileSync,
11+
rmSync,
12+
rmdirSync,
13+
renameSync
14+
} from 'graceful-fs'
615

716
jest.mock('electron')
817
jest.mock('../logger/logger')
@@ -131,4 +140,150 @@ describe('backend/utils.ts', () => {
131140
expect(releases).toMatchInlineSnapshot(`Array []`)
132141
})
133142
})
143+
144+
describe('calculateEta', () => {
145+
const getBytesFromMB = (sizeInMB: number) => sizeInMB * 1024 * 1024
146+
test('normal download seconds', () => {
147+
const downloadedMB = 10
148+
const totalSizeMB = 100
149+
const downloadSpeedMB = 10
150+
const etaString = utils.calculateEta(
151+
getBytesFromMB(downloadedMB),
152+
getBytesFromMB(downloadSpeedMB),
153+
getBytesFromMB(totalSizeMB)
154+
)
155+
expect(etaString).toEqual('00:00:09')
156+
})
157+
158+
test('downloaded > total size', () => {
159+
const downloadedMB = 105
160+
const totalSizeMB = 100
161+
const downloadSpeedMB = 10
162+
const etaString = utils.calculateEta(
163+
getBytesFromMB(downloadedMB),
164+
getBytesFromMB(downloadSpeedMB),
165+
getBytesFromMB(totalSizeMB)
166+
)
167+
expect(etaString).toEqual('00:00:00')
168+
})
169+
170+
test('with last progress time 4 seconds ago', () => {
171+
const downloadedMB = 10
172+
const totalSizeMB = 100
173+
const downloadSpeedMB = 10
174+
const lastProgressTime = Date.now().valueOf() - 1000 * 4
175+
const etaString = utils.calculateEta(
176+
getBytesFromMB(downloadedMB),
177+
getBytesFromMB(downloadSpeedMB),
178+
getBytesFromMB(totalSizeMB),
179+
lastProgressTime
180+
)
181+
expect(etaString).toEqual('00:00:05')
182+
})
183+
184+
test('normal download minutes', () => {
185+
const downloadedMB = 100
186+
const totalSizeMB = 1000
187+
const downloadSpeedMB = 10
188+
const etaString = utils.calculateEta(
189+
getBytesFromMB(downloadedMB),
190+
getBytesFromMB(downloadSpeedMB),
191+
getBytesFromMB(totalSizeMB)
192+
)
193+
expect(etaString).toEqual('00:01:30')
194+
})
195+
196+
test('normal download hours', () => {
197+
const downloadedMB = 100
198+
const totalSizeMB = 100000
199+
const downloadSpeedMB = 10
200+
const etaString = utils.calculateEta(
201+
getBytesFromMB(downloadedMB),
202+
getBytesFromMB(downloadSpeedMB),
203+
getBytesFromMB(totalSizeMB)
204+
)
205+
expect(etaString).toEqual('02:46:30')
206+
})
207+
})
208+
209+
describe('bytesToSize', () => {
210+
test('Bytes', () => {
211+
expect(utils.bytesToSize(0)).toEqual('0 Bytes')
212+
expect(utils.bytesToSize(10)).toEqual('10 Bytes')
213+
expect(utils.bytesToSize(112)).toEqual('112 Bytes')
214+
})
215+
216+
test('Kilobytes', () => {
217+
expect(utils.bytesToSize(1024)).toEqual('1 KB')
218+
expect(utils.bytesToSize(1025)).toEqual('1 KB')
219+
expect(utils.bytesToSize(2059)).toEqual('2.01 KB')
220+
})
221+
222+
test('Megabytes', () => {
223+
expect(utils.bytesToSize(1024 * 1024)).toEqual('1 MB')
224+
expect(utils.bytesToSize(1025 * 1024)).toEqual('1 MB')
225+
expect(utils.bytesToSize(2059 * 1024)).toEqual('2.01 MB')
226+
})
227+
228+
test('Gigabytes', () => {
229+
expect(utils.bytesToSize(1024 * 1024 * 1029)).toEqual('1 GB')
230+
expect(utils.bytesToSize(1025 * 1024 * 2056)).toEqual('2.01 GB')
231+
expect(utils.bytesToSize(2059 * 1024 * 3045)).toEqual('5.98 GB')
232+
})
233+
234+
test('Terabytes', () => {
235+
expect(utils.bytesToSize(1024 * 1024 * 1029 * 44000)).toEqual('43.18 TB')
236+
expect(utils.bytesToSize(1025 * 1024 * 2056 * 21010)).toEqual('41.24 TB')
237+
expect(utils.bytesToSize(2059 * 1024 * 3045 * 4000)).toEqual('23.36 TB')
238+
})
239+
})
240+
241+
describe('extractZip', () => {
242+
let testCopyZipPath: string
243+
let destFilePath: string
244+
245+
beforeEach(() => {
246+
const testZipPath = path.resolve('./src/backend/__mocks__/test.zip')
247+
//copy zip because extract will delete it
248+
testCopyZipPath = path.resolve('./src/backend/__mocks__/test2.zip')
249+
copyFileSync(testZipPath, testCopyZipPath)
250+
destFilePath = path.resolve('./src/backend/__mocks__/test')
251+
})
252+
253+
afterEach(async () => {
254+
const extractPromise = utils.extractZip(testCopyZipPath, destFilePath)
255+
await extractPromise
256+
expect(extractPromise).resolves
257+
258+
const testTxtFilePath = path.resolve(destFilePath, './test.txt')
259+
console.log('checking dest file path ', testTxtFilePath)
260+
expect(existsSync(testTxtFilePath)).toBe(true)
261+
262+
const testMessage = readFileSync(testTxtFilePath).toString()
263+
console.log('unzipped file contents: ', testMessage)
264+
expect(testMessage).toEqual('this is a test message')
265+
266+
//extract deletes the zip file used to extract async so we wait and then check
267+
await utils.wait(100)
268+
expect(existsSync(testCopyZipPath)).toBe(false)
269+
270+
//clean up test
271+
rmSync(testTxtFilePath)
272+
rmdirSync(destFilePath)
273+
expect(existsSync(testTxtFilePath)).toBe(false)
274+
expect(existsSync(destFilePath)).toBe(false)
275+
})
276+
277+
test('extract a normal test zip', async () => {
278+
console.log('extracting test.zip')
279+
})
280+
281+
test('extract a test zip with non ascii characters', async () => {
282+
const renamedZipFilePath = path.resolve(
283+
'./src/backend/__mocks__/谷���新道ひばりヶ�.zip'
284+
)
285+
renameSync(testCopyZipPath, renamedZipFilePath)
286+
testCopyZipPath = renamedZipFilePath
287+
})
288+
})
134289
})

src/backend/main.ts

-2
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ import {
105105
import { handleProtocol } from './protocol'
106106
import {
107107
logChangedSetting,
108-
logDebug,
109108
logError,
110109
logInfo,
111110
LogPrefix,
@@ -592,7 +591,6 @@ ipcMain.handle('checkDiskSpace', async (event, folder) => {
592591
message: `${getFileSize(free)} / ${getFileSize(diskSize)}`,
593592
validPath: !writeError
594593
}
595-
logDebug(`${JSON.stringify(ret)}`, LogPrefix.Backend)
596594
res(ret)
597595
})
598596
})

src/backend/storeManagers/hyperplay/games.ts

+43-56
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
downloadFile,
1818
spawnAsync,
1919
killPattern,
20-
shutdownWine
20+
shutdownWine,
21+
extractZip,
22+
calculateEta
2123
} from 'backend/utils'
2224
import { notify } from 'backend/dialog/dialog'
2325
import path, { join } from 'path'
@@ -173,6 +175,10 @@ export async function importGame(
173175
const installDistributables = async (gamePath: string) => {
174176
const distFolder = path.join(gamePath, 'dist')
175177
if (!existsSync(distFolder)) {
178+
logWarning(
179+
`Tried to install distributables from ${distFolder} but folder does not exist!`,
180+
LogPrefix.HyperPlay
181+
)
176182
return
177183
}
178184

@@ -215,9 +221,11 @@ async function downloadGame(
215221
downloadPath,
216222
createAbortController(appName),
217223
(downloadedBytes, downloadSpeed, diskWriteSpeed, progress) => {
218-
// convert speed to Mb/s
219-
downloadSpeed = Math.round(downloadSpeed / 1000000)
220-
diskWriteSpeed = Math.round(diskWriteSpeed / 1000000)
224+
const eta = calculateEta(
225+
downloadedBytes,
226+
downloadSpeed,
227+
platformInfo.downloadSize
228+
)
221229

222230
window.webContents.send(`progressUpdate-${appName}`, {
223231
appName,
@@ -226,10 +234,11 @@ async function downloadGame(
226234
folder: destinationPath,
227235
progress: {
228236
percent: progress,
229-
diskSpeed: diskWriteSpeed,
230-
downSpeed: downloadSpeed,
231-
bytes: downloadedBytes,
232-
folder: destinationPath
237+
diskSpeed: diskWriteSpeed / 1024 / 1024,
238+
downSpeed: downloadSpeed / 1024 / 1024,
239+
bytes: downloadedBytes / 1024 / 1024,
240+
folder: destinationPath,
241+
eta
233242
}
234243
})
235244
}
@@ -264,19 +273,19 @@ export async function install(
264273

265274
logInfo(`Installing ${title} to ${dirpath}...`, LogPrefix.HyperPlay)
266275

267-
// download the zip file
268-
try {
269-
const appPlatform = handleArchAndPlatform(platformToInstall, releaseMeta)
270-
const platformInfo = releaseMeta.platforms[appPlatform]
271-
const zipName = encodeURI(platformInfo.name)
272-
const tempfolder = path.join(configFolder, 'hyperplay', '.temp', appName)
276+
const appPlatform = handleArchAndPlatform(platformToInstall, releaseMeta)
277+
const platformInfo = releaseMeta.platforms[appPlatform]
278+
const zipName = encodeURI(platformInfo.name)
279+
const tempfolder = path.join(configFolder, 'hyperplay', '.temp', appName)
273280

274-
if (!existsSync(tempfolder)) {
275-
mkdirSync(tempfolder, { recursive: true })
276-
}
281+
if (!existsSync(tempfolder)) {
282+
mkdirSync(tempfolder, { recursive: true })
283+
}
277284

278-
const zipFile = path.join(tempfolder, zipName)
285+
const zipFile = path.join(tempfolder, zipName)
279286

287+
// download the zip file
288+
try {
280289
// prevent naming conflicts where two developers release games with the same name
281290
const sanitizedDestinationFolderName =
282291
developer !== undefined
@@ -300,33 +309,10 @@ export async function install(
300309
})
301310

302311
if (isWindows) {
303-
await spawnAsync('powershell', [
304-
'Expand-Archive',
305-
'-LiteralPath',
306-
`"${zipFile}"`,
307-
'-DestinationPath',
308-
`"${destinationPath}"`
309-
])
310-
312+
await extractZip(zipFile, destinationPath)
311313
await installDistributables(destinationPath)
312314
} else {
313-
// extract the zip file and overwrite existing files
314-
const { code, stderr } = await spawnAsync('unzip', [
315-
'-o',
316-
zipFile,
317-
'-d',
318-
destinationPath
319-
])
320-
if (code !== 0) {
321-
rmSync(zipFile)
322-
clean(zipFile)
323-
throw new Error(stderr)
324-
}
325-
}
326-
rmSync(zipFile)
327-
328-
if (isWindows) {
329-
await installDistributables(destinationPath)
315+
await extractZip(zipFile, destinationPath)
330316
}
331317

332318
if (isMac && executable.endsWith('.app')) {
@@ -377,6 +363,8 @@ export async function install(
377363
return { status: 'done' }
378364
} catch (error) {
379365
logInfo('Error while downloading and extracting game', LogPrefix.HyperPlay)
366+
rmSync(zipFile)
367+
clean(zipFile)
380368
return {
381369
status: 'error',
382370
error: `${error}`
@@ -474,17 +462,19 @@ export async function removeShortcuts(appName: string): Promise<void> {
474462
}
475463

476464
export async function getExtraInfo(appName: string): Promise<ExtraInfo> {
477-
logWarning(
478-
`getExtraInfo not implemented on HyperPlay Game Manager. called for appName = ${appName}`
479-
)
480-
return {
481-
about: {
482-
description: '',
483-
shortDescription: ''
484-
},
485-
reqs: [],
486-
storeUrl: ''
465+
const extraInfo = getGameInfo(appName).extra
466+
if (!extraInfo) {
467+
logWarning(`No extra info found for ${appName}`, LogPrefix.HyperPlay)
468+
return {
469+
about: {
470+
description: '',
471+
shortDescription: ''
472+
},
473+
reqs: [],
474+
storeUrl: ''
475+
}
487476
}
477+
return extraInfo
488478
}
489479

490480
export async function launch(
@@ -565,8 +555,5 @@ export async function syncSaves(
565555
}
566556

567557
export async function forceUninstall(appName: string): Promise<void> {
568-
logWarning(
569-
`forceUninstall not implemented on HyperPlay Game Manager. Calling uninstall instead called for appName = ${appName}`
570-
)
571558
await uninstall({ appName, shouldRemovePrefix: false })
572559
}

src/backend/storeManagers/hyperplay/utils.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,14 @@ export function handleArchAndPlatform(
6767

6868
export function handlePlatformReversed(platform: string) {
6969
switch (platform) {
70-
case 'windows_arm64':
71-
return 'Windows'
72-
case 'linux_arm64':
73-
return 'linux'
74-
case 'darwin_arm64':
75-
return 'Mac'
7670
case 'windows_amd64':
71+
case 'windows_arm64':
7772
return 'Windows'
7873
case 'linux_amd64':
79-
return 'linux'
74+
case 'linux_arm64':
75+
return 'Linux'
8076
case 'darwin_amd64':
77+
case 'darwin_arm64':
8178
return 'Mac'
8279
case 'web':
8380
return 'Browser'

0 commit comments

Comments
 (0)