Compare commits

..

7 commits

Author SHA1 Message Date
quexeky
9fab0af4e8
fix: Removed unnecessary nightly feature try_trait_v2 2025-01-28 22:48:26 +11:00
quexeky
3968a61786
fix(downloads): Fix rearranging download queue throwing error 2025-01-25 23:20:31 +11:00
quexeky
8861fe4e3d
chore: Version bump 2025-01-25 19:46:58 +11:00
quexeky
f5bd12b43a
Merge develop into main (#25)
* chore(process manager): refactor for generic way to implement cross
platform launchers

* feat(game): game uninstalling & partial compat

* chore(metadata): update metadata

* feat(errors): better download manager errors + modal

* feat(process): better process management, including running state

* feat(downloads): lockless tracking of downloaded chunks

* fix(sign on): add message about nonce expiration

* feat(download ui): add speed and time remaining information

closes #7

Co-authored-by: AdenMGB <140392385+AdenMGB@users.noreply.github.com>

* chore: Ran cargo clippy

Signed-off-by: quexeky <git@quexeky.dev>

* fix(auth initiate): add better error message

* feat(auth): offer manual signin

* feat(install modal): add note about more install dirs

* fix(install flow): clear stale data before requesting new

* Delete pages/library.vue

* Add files via upload

* adds nvm rc!

* feat(install modal): add note about more install dirs

* fix(install flow): clear stale data before requesting new

* Delete pages/library.vue

* Add files via upload

* fix(library page): fix install button

* fix(process): fix poorly designed parsing for executables with spaces

* fix(scrollbars): fix ugly scrollbars on edge webview

* feat(Compat): Implemented spawning with umu (using umu-wrapper-lib)

Signed-off-by: quexeky <git@quexeky.dev>

* feat(process manager): Game kill tauri command

Signed-off-by: quexeky <git@quexeky.dev>

* fix(deep links): Re-enabled deep links

Signed-off-by: quexeky <git@quexeky.dev>

* feat(process): shared child with stop command

* squash(autostart): added adenmgb's autostart feature

Squashed commit of the following:

commit 085cd9481dee748ee84c1a8f9dcd8ef0b01105ab
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 16:29:41 2024 +1030

    Update lib.rs for the DB sync of autostart

commit 86f2fb19bde3933ec6b5cd82701de0d306121659
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 16:29:13 2024 +1030

    Update db.rs to accomidate the settings sync

commit ece11e7581c9aef55588b2e829379b1224021a45
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 16:27:48 2024 +1030

    Update autostart.rs to include DB

commit 7ea8a24fdc2ff98379694ce8e347a40fcfd5fea3
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:17:38 2024 +1030

    Add files via upload

commit af2f232d94fa449d9e20df737ce81ebded87fd50
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:17:09 2024 +1030

    Delete src-tauri/Cargo.toml

commit 5d27b65612457de6eb30835d5423b9d96fd5a596
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:15:42 2024 +1030

    Add files via upload

commit 2eea7b97a876e23cc0d6daec37f1b75af9bf3ae2
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:15:31 2024 +1030

    Delete src-tauri/src/lib.rs

commit 9a635a10d1340f86c74812113284b115b34b9bbe
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:14:49 2024 +1030

    Add files via upload

commit 2fb049531a082fbdd217aba694819b7a3f954a55
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:13:37 2024 +1030

    Add files via upload

commit ea1be4d7505a9ab16bda338491c0ec313d0bc586
Author: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
Date:   Mon Dec 30 15:13:20 2024 +1030

    Delete pages/settings/index.vue

* fix(download manager): fix incorrect error assumptions & update types

* feat(account settings): Add signout functionality (#16)

* Create account.vue with logout button

* Update auth.rs to add signout command

* Update lib.rs to pass sign_out command to frontend

* feat(settings): add debug page

* Create debug.rs

* Update settings.vue to add tab for debug

* Update main.scss to add light theme

* Update interface.vue to add light mode

* Create debug.vue

* Update debug.vue too add open log button

* Update lib.rs

* Update debug.rs

* Update debug.rs

* Update lib.rs

* Update lib.rs

* Update debug.rs

* Update debug.vue

* fix(debug): refactor and cleanup

* revert(theme): revert light theming

---------

Co-authored-by: DecDuck <declanahofmeyr@gmail.com>

* feat(library ui): add installed ui in the library menu

* chore(tool manager): Progress on adding tools

Going to try changing around the download manager to take a generic trait rather than specifically for game downloads

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Moved download manager to separate directory

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Added Downloadable trait and replaced references to GameDownloadAgent

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Renamed most instances of "game" outside of actual game downloads

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Renamed GameDonwloadError to ApplicationDownloadError and moved

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Some easy cleanup of the download manager

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Ensure that Downloadable is also send and sync

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Moved manifest and stored_manifest to download_manager

Signed-off-by: quexeky <git@quexeky.dev>

* Revert "refactor(download manager): Moved manifest and stored_manifest to download_manager"

This reverts commit 8db2393346.

* chore(tool manager): Added ToolDownloadAgent

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Added manage_queue_signal

Signed-off-by: quexeky <git@quexeky.dev>

* chore(download manager): Added manage_go_signal command

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Removed all references to anything outside of the DownloadManager

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Fully separate & generic download manager

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(download manager): Removed Arc requirement for DownloadableMetadata

Signed-off-by: quexeky <git@quexeky.dev>

* feat(download manager): Added generic download manager

Signed-off-by: quexeky <git@quexeky.dev>

* fix(game launcher): Renamed game_id to id

Signed-off-by: quexeky <git@quexeky.dev>

* fix(uninstalling): Re-enabled uninstalling apps

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(downloads): Moved all files relevant to game downloads to their own directory

Signed-off-by: quexeky <git@quexeky.dev>

* fix(kill game): Re-enabled killing games

Signed-off-by: quexeky <git@quexeky.dev>

* feat(recovery): Added database recovery

Signed-off-by: quexeky <git@quexeky.dev>

* feat(database): Added database corruption dialog

Signed-off-by: quexeky <git@quexeky.dev>

* chore(README): Updated README.md

Signed-off-by: quexeky <git@quexeky.dev>

* perf(game downloads): Moved some variable declarations outside of the spawned download thread

Signed-off-by: quexeky <git@quexeky.dev>

* fix(game downloads): Accidentally was attempting to lock onto something that was already in scope

Signed-off-by: quexeky <git@quexeky.dev>

* fix(db): Added Settings component

Signed-off-by: quexeky <git@quexeky.dev>

* refactor: Ran cargo clippy & fmt

Signed-off-by: quexeky <git@quexeky.dev>

* chore: More cleanup after cargo clippy

Also added some type efficiency improvements (using references where possible and added SliceDeque crate)

Signed-off-by: quexeky <git@quexeky.dev>

* feat(settings): Added max_download_threads setting and separated settings from db

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Moved generateGameMeta.ts to composables, using PathBuf instead of String for install_dirs

Signed-off-by: quexeky <git@quexeky.dev>

* chore: General cleanup

- Changed some info!() statements to debug!() and warn!()
- Removed most Turbofish syntax cases
- Removed InvalidCodeError and replaced it with InvalidResponse

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Removed tests/

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Removed tools/

Signed-off-by: quexeky <git@quexeky.dev>

* chore: More refining info!() statements

Signed-off-by: quexeky <git@quexeky.dev>

* feat(download manager): Added UI to change download threads

Co-authored-by: AdenMGB <140392385+AdenMGB@users.noreply.github.com>
Signed-off-by: quexeky <git@quexeky.dev>

* fix(metadata): update routes for new server

* fix(handle invalid database): use set_file_name instead of pushing to
strings

* refactor(compat): remove unnecessary compat code (#20)

* Delete pages/settings/compatibility.vue

* Update settings.vue

* Update debug.vue

* Update lib.rs

* Update compat.rs

* feat(debug): use shift or DEBUG RUST_LOG to show Debug Info

* Update settings.vue to have a conditional debug page

* Update debug.rs to add RUST_LOG status fetching

* Implement better error system and segregate errors and commands (#23)

* chore: Progress on amend_settings command

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): Progress on better error handling with segragation of files

* chore: Progress on amend_settings command

Signed-off-by: quexeky <git@quexeky.dev>

* chore(commands): Separated commands under each subdirectory into respective commands.rs files

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): Almost all errors and commands have been segregated

* chore(errors): Added drop server error

Signed-off-by: quexeky <git@quexeky.dev>

* feat(core): Update to using nightly compiler

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): More progress on error handling

Signed-off-by: quexeky <git@quexeky.dev>

* chore(errors): Implementing Try and FromResidual for UserValue

Signed-off-by: quexeky <git@quexeky.dev>

* refactor(errors): Segregated errors and commands from code, and made commands return UserValue struct

Signed-off-by: quexeky <git@quexeky.dev>

* fix(errors): Added missing files

* chore(errors): Convert match statement to map_err

* feat(settings): Implemented settings editing from UI

* feat(errors): Clarified return values from retry_connect command

* chore(errors): Moved autostart commands to autostart.rs

* chore(process manager): Converted launch_process function for games to use game_id

---------

Signed-off-by: quexeky <git@quexeky.dev>

* fix(settings): Broken command invoke logic in settings/downloads.vue

* feat(logging): Added line numbers to file logging and highlighting to console

* chore(progress): Added rolling_progress_updates.rs

Signed-off-by: quexeky <git@quexeky.dev>

* chore(exit): Progress on cleanup and exit

* chore(downloads): Progress on terminator

* chore: Progress on rolling progress window

* feat(progress): Added rolling progress window

Still needs tweaks on specific timings, as well as cleanup

* refactor(remote): Created separate function to generate requests

* fix(install ui): stop loading on error

* fix: fix other metadata endpoints

* feat(errors): Using SerializeDisplay for better error management with Result

* chore: Update .gitlab-ci.yml

* refactor(logging): Using more appropriate logging statements

Still probably needs some work, but that's enough for now

* chore(logging): Imported appropriate logging macros

* Revert "chore: Update .gitlab-ci.yml"

This reverts commit fc6bab9381.

* feat(settings): Allow settings to update UI using fetch_settings command

* style(logging): Ensured that all logs start with lowercase capital and have no trailing punctuation

* fix(download manager): don't crash download manager if multiple errors
come in

* feat(downloads): re-enable checksums

* fix(logs): add file & line to console logs

* fix(ui): modal stack doesn't cover whole app

* feat(database): Ensure that any database issues are resolved by standalone functions

Functions are as follows:
- save_db()
- borrow_db_checked()
- borrow_db_mut_checked()

* chore: Ran cargo clippy & cargo fmt

* fix: assorted fixes

* fix(download agent): fixed completed indexes

* fix: Adding usize to completed_contexts_lock instead of &usize

* fix(game downloads): Added error handling for chunk request errors

* chore: Apply stashed changes

* feat(games): Added multi-argument game launch and setup support

* fix: Games not launching due to string semantics

* build: Version bump & appimage build

* chore: Update .gitlab-ci.yml

* Update .gitlab-ci.yml

* Update .gitlab-ci.yml with artifacts

* feat(settings): Made save button include user feedback & only allow numeric characters

* fix(library): Added "LIbrary Failed to Update" content to recover from library load fail

* fix(logging): Restored RUST_LOG env functionality

* Update changelog.md

---------

Signed-off-by: quexeky <git@quexeky.dev>
Signed-off-by: DecDuck <declanahofmeyr@gmail.com>
Co-authored-by: DecDuck <declanahofmeyr@gmail.com>
Co-authored-by: AdenMGB <140392385+AdenMGB@users.noreply.github.com>
Co-authored-by: seethruhead <shane.keulen@gmail.com>
2025-01-25 18:49:54 +11:00
quexeky
6e4ac4ad83
Update issue templates 2025-01-21 10:17:46 +11:00
quexeky
40c4e8a71c
Update .gitlab-ci.yml file 2025-01-21 10:17:46 +11:00
quexeky
4ad688da14
refactor(downloads): Replaced static usage with const 2025-01-21 10:17:30 +11:00
84 changed files with 2305 additions and 1497 deletions

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Arch Linux, Windows]
- App Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -3,7 +3,7 @@ stages:
build-linux:
stage: build
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rust:1.81.0-bookworm
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rustlang/rust:nightly
script:
- apt-get update -y
- apt-get install yarnpkg libsoup-3.0-0 libsoup-3.0-dev libatk-adaptor libgtk-3-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev -y
@ -13,9 +13,10 @@ build-linux:
- yarnpkg tauri build
- cp src-tauri/target/release/bundle/deb/*.deb .
- cp src-tauri/target/release/bundle/rpm/*.rpm .
- cp src-tauri/target/release/bundle/appimage/*.AppImage .
artifacts:
paths:
- "*.{deb,rpm}"
- "*.{deb,rpm,AppImage}"
build-windows:
stage: build

14
app.vue
View file

@ -1,8 +1,8 @@
<template>
<NuxtLayout class="select-none w-screen h-screen">
<NuxtPage />
<ModalStack />
</NuxtLayout>
<ModalStack />
</template>
<script setup lang="ts">
@ -20,10 +20,18 @@ import {
const router = useRouter();
const state = useAppState();
state.value = JSON.parse(await invoke("fetch_state"));
try {
state.value = JSON.parse(await invoke("fetch_state"));
} catch (e) {
console.error("failed to parse state", e);
}
router.beforeEach(async () => {
state.value = JSON.parse(await invoke("fetch_state"));
try {
state.value = JSON.parse(await invoke("fetch_state"));
} catch (e) {
console.error("failed to parse state", e);
}
});
setupHooks();

View file

@ -1,5 +1,292 @@
## Release 0.2.0-beta
### Fixes
- Re-enabled killing games #005bab2
- fixed queue manipulation and waiting for downloads #01260f0
- fix logic error in detecting dir #04368ff
- absolute executable invoke #17759c4
- don't crash download manager if multiple errors come in #21204de
- clear stale data before requesting new #327628b
- fixed completed indexes #39f2ebd
- add file & line to console logs #4d8eadc
- Games not launching due to string semantics #4ef49cc
- Added error handling for chunk request errors #4fc0855
- Chunk counting logic error #5ba151f
- modal stack doesn't cover whole app #5db9ae5
- use set_file_name instead of pushing to strings #60d0a48
- use of completed signal, and pause/resuming #64d7f64
- add message about nonce expiration #6a8d0af
- Added "LIbrary Failed to Update" content to recover from library load fail #76bae3d
- Restored RUST_LOG env functionality #7a0cf4f
- initialise doesn't recreate default install dir #7a3841b
- update routes for new server #7ab53f3
- use vendored flag #7c8089e
- fix poorly designed parsing for executables with spaces #7c90d2b
- assorted fixes #89ea34c
- Added Settings component #8aad64f
- windows build #8d9234f
- fix ugly scrollbars on edge webview #95f2174
- windows shadow #9a8cc59
- add better error message #9af0d08
- Broken command invoke logic in settings/downloads.vue #9e29aa7
- Accidentally was attempting to lock onto something that was already in scope #9e82a0b
- fix incorrect error assumptions & update types #a17311a
- Re-enabled uninstalling apps #a56ee25
- types #af056c0
- fix other metadata endpoints #c2f54c1
- Re-enabled deep links #c3f6222
- added console as an appender #d12bf15
- remove unnecessary unstable feature #d5ac1b0
- fix install button #d7b0302
- stop loading on error #d83aae6
- use unix timestamp to avoid invalid characters in filename #dafce24
- Renamed game_id to id #dceaa56
- use chrono library to generate timestamps #e22e6d8
- clear stale data before requesting new #e72662c
- fix scrollbars on edge webview #f09605a
- update readme instructions #f0c47d8
- Adding usize to completed_contexts_lock instead of &usize #f508186
### Features
- Game kill tauri command #01e6162
- add debug page #02f8591
- Add signout functionality (#16) #0a0d9d6
- queue and library UIs #0a20139
- add note about more install dirs #139bc0c
- Using SerializeDisplay for better error management with Result #170fde5
- add pre-launch log to file #17f8d76
- Added option to change root directory #1aa52c0
- add speed and time remaining information #1f899ec
- lockless tracking of downloaded chunks #2183585
- quit button #239b8d5
- use shift or DEBUG RUST_LOG to show Debug Info #245a84d
- Added database corruption dialog #25ba200
- only allow downloads for supported platforms #269dcbb
- add installed ui in the library menu #2c8164e
- added file-based logging #2d4a7e8
- automatically fetch remote data if not available #2dedfbb
- Added database recovery #32ae7d5
- ability to add more download dirs #384f7a5
- re-enable checksums #3ca87fc
- background processes and close/open menu #3d60fd5
- launch games with log files #3f71149
- Download cancelling #450bca9
- refactoring and error message #469a2d6
- Added UI to change download threads #4e93eb4
- Made save button include user feedback & only allow numeric characters #53234d2
- download widget and queue fix #532d13e
- Pausing and resuming game downloads #55b7921
- Allow settings to update UI using fetch_settings command #5bb04da
- temporary queue ui and flamegraph instructions #5cbeb3b
- Added DownloadThreadControl struct #5e05e68
- Added max_download_threads setting and separated settings from db #5ea47d7
- Added generic download manager #6159319
- Added AgentInterfaceData to get information about all downloads in queue #63c3cc1
- debug queue interface #671d45f
- reduce scope of download agent #6a38ea3
- Added multi-argument game launch and setup support #6ad3837
- shared child with stop command #6b96e40
- Added function to take and set any game state #6bc6482
- Added line numbers to file logging and highlighting to console #7c3140e
- Separated chunk updates into individual counters #7d3c601
- Ensure that any database issues are resolved by standalone functions #7d4651d
- ui to install games #8670bca
- Implemented spawning with umu (using umu-wrapper-lib) #88b2505
- offer manual signin #949acfc
- better process management, including running state #a135b13
- Added Download Manager #a1ada07
- retry connnection on server unavailable #a53d838
- finish download dir CRUD interface #a580a46
- better download manager errors + modal #ad92dbe
- syncs state to disk to persist across reboots #b556842
- prevent default context menu and emit event on elements #c560656
- initial creation and logo update #d9a51cf
- Added manifest.json utility for persistent download progress #d9d0122
- game uninstalling & partial compat #dd7f567
- combined db and download interface improvements #de52dac
- update db state with ui and emit events #e4df4eb
- Generic function to set download state #f10d92d
- Convert DownloadThreadControlFlag to AtomicBool #f25bfed
- add note about more install dirs #f4ac1c8
- Added rolling progress window #fd30b3e
### Other Changes
- quexeky <git@quexeky.dev>
- Convert DATA_ROOT_DIR to Mutex #00b7179
- Converting DB access to a trait #01b092c
- Updated changelog #022330b
- Progress on cleanup and exit #0381b8b
- library ui #03fa364
- Scoping changes and removing qualifications #046ba64
- Moved all files relevant to game downloads to their own directory #06d1e9e
- SLowly integrating game_download into the FE. Started with using the manifest minimal example in the server (#1) #07379b2
- Ran cargo clippy & moved DownloadManagerInterface #075d6ec
- Made logging systems match #0a1dddf
- Some easy cleanup of the download manager #0a2ac25
- client now fetches user information from Drop server #0c0cfeb
- Included in AppStatus (Also trying to link to Issue #1)
- Accidentally serialized AppStatus and broke everything :/ #10791ed
- Removed debugging statements #10c8344
- Wrappers are the bane of my existence. Also here's the download cancelling logic. #13df631
- Merge branch 'error-handling' #1520471
- Updated README.md #165a967
- Removed unnecessary dependencies #1724449
- merge(download-manager) -> 'main' #172d6b0
- More refactoring and renaming camelCase struct definitions to snake_case #1742793
- General cleanup #182361e
- Delete pages/library.vue #1861659
- progress on more precise download control #18b9149
- Allowing some dead code features because they are there for future use (potentially) #191e62c
- Ensure that Downloadable is also send and sync #1a89135
- I think that download queuing is working #1ab61c8
- auth initiate, database and more #22b1aee
- Update .gitlab-ci.yml" #2307704
- More fleshing out on how specifically game downloads will work (#1) #23137dd
- Removed utils.rs #270bc8b
- Fixing some references to "id" vs "game_id" #27e5a8e
- More cleanup after cargo clippy #2822b7a
- Updated contributing link #2aa5b9c
- More fleshing out on how specifically game downloads will work #2b90de9
- Cleaning up downloads playing and pausing #2c7b5fb
- fixed multi-chunk downloads #2ec351f
- Clippy refactoring #2efe304
- remove unpacker mod statement #32067c0
- Progress on adding tools #3299c71
- Fixed bug with bad initial loading into store instead of auth #3923acf
- add nvm rc #3ccd444
- partial download manager #3dbf5ab
- Update .gitlab-ci.yml with artifacts #3e10f17
- Removed tools/ #3eda979
- Downloads should be fixed now #403ca65
- transient vs synced state now defined #42c0198
- added adenmgb's autostart feature #472eb1d
- better download defaults #4779383
- Progress on downloads. Currently working on parsing functions to be run asynchronously #496c6a5
- Ran cargo clippy & cargo fmt #4983b25
- handshakes #4bb33c8
- Convert DOWNLOAD_MAX_THREADS to const #4fc13a1
- Merge branch 'downloads' #50ed841
- Moved generateGameMeta.ts to composables, using PathBuf instead of String for install_dirs #50f37fd
- Added time debugging and fixed logging formatting #5243694
- Clippy changes #553bc37
- Queue is running game downloads sequentially now #5564d23
- migrate to new droplet ca system #556898f
- Add LICENSE #57a5737
- ran cargo clippy & cargo fmt #5e3d26b
- my own take on some BASED design decisions #5ed0833
- cleanup and game UI beginnings #5ef6b8e
- Progress on terminator #5f5cbd0
- Implement better error system and segregate errors and commands (#23) #604d5b5
- moved to completed index arr to help serialization #64ebc19
- Ran cargo clippy & cargo fmt #653717e
- Removed all references to anything outside of the DownloadManager #6568faa
- Merge remote-tracking branch 'origin/main' #68ca4a7
- swap file name and to binary encoding #694f2fd
- chore(polish & cleanup) #6cc0c67
- Update .gitlab-ci.yml #6d7630e
- Moved some variable declarations outside of the spawned download thread #6ea4cf2
- Encoding game IDs and versions #6ef444e
- restructing and renaming #7049673
- Converted to md5 #706f525
- Merge branch 'main' into downloads #714b968
- Semantic naming changes #725f16b
- Abstracted queue system #76b0975
- Moved manifest and stored_manifest to download_manager" #78149bb
- README update #78fc668
- Ensured everything is serializing/deserializing to camelCase #7a95b7f
- fixed some of quexeky's BASED design decisions #7e3da04
- Progress checker works #7fec00d
- Progress on refactoring and abiding by cargo clippy #816b427
- Added GAME_PAUSE_CHECK_INTERVAL value #8204795
- Ran cargo clippy & fmt #82804eb
- update metadata #85a0899
- Renamed most instances of "game" outside of actual game downloads #881fcc6
- Debugging & starting work on parsing manifest #89d2814
- slight ui/ux fixes and updates to auth protocol #8a2d23d
- Removed Arc requirement for DownloadableMetadata #8be1dd4
- compliant with new APIs #8f6f184
- Ran cargo clippy & cargo fmt #9272970
- Added rolling_progress_updates.rs #9369ff1
- Add files via upload #93b8b83
- More refining info!() statements #94cf678
- fixed windows issues #959dad3
- Starting p2p progress #97bb1fa
- Game downloads from the client are working (multithreaded) by parsing in gameID, GameVersion, and maxThreads from FE (#1) #984472e
- Version bump & appimage build #9897698
- Some progress on thread terminations #99beca4
- rename files to what they contain #99c8b39
- Created separate function to generate requests #9a184a8
- cleanup of lib and toml #9b1cfa7
- refactor for generic way to implement cross platform launchers #9ea2aa4
- Updated logging format #a213765
- fix(windows build) #a24cc8a
- Added ToolDownloadAgent #a2e63aa
- copy direct to disk #a628fc1
- Moved manifest and stored_manifest to download_manager #a846eed
- adds nvm rc! #a881d8e
- Reordered DownloadThreadControlFlag to agree with From<bool> #ab606e8
- ci/cd and patches for windows builds #ac1c3b6
- patch for not draggable windows during setup #ac66b20
- another stage of client authentication #ae4c65b
- Renamed GameDonwloadError to ApplicationDownloadError and moved #aed58e4
- Progress on write speeds & added debug statements #b065e10
- Updated logging #b3963b6
- Created file settings.rs #b47b7ea
- Added Downloadable trait and replaced references to GameDownloadAgent #b4d70a3
- Update .gitlab-ci.yml #b6a54c0
- Moved download manager to separate directory #b6c64e5
- Ran cargo fmt #b8cf44c
- Imported appropriate logging macros #b99ff67
- Merge branch 'main' into download-manager #bb60942
- Ran cargo clippy & cargo fmt #bd3deac
- beginnings of game state management #bf46dec
- Update Cargo.toml #c1fb39e
- migrated unpacking to rust zstd to conform with droplet #c46c54b
- More progress on checksums #c51e761
- Delete pages/library.vue #c722a54
- Merge branch 'downloads' (again) #c748aec
- migrate to nuxt and groundwork #c957744
- More debugging because apparently checksums are the bane of my existence. But it works and I was just an idiot #c9d9d2e
- Fully separate & generic download manager #cac612b
- Progress on rolling progress window #cf19477
- Ensured that all logs start with lowercase capital and have no trailing punctuation #cfc9d13
- Validated that loading data works #d21b1d2
- Mostly finished with checksums. Just merging main in at the same time #d39e7cb
- Ran cargo clippy #dcb1564
- Add files via upload #dcb2c0f
- Theoretically adding queue support and optimistic manifest downloading (#1). Needs tests when actual functions are implemented #dcd8fa8
- Merge remote-tracking branch 'origin/downloads' into downloads #dd23ca8
- Debugging line #ddc585d
- Re-enabled closing the window and some more renaming #defba51
- drop no longer freaks out if server is unavailable on startup #df88395
- Apply stashed changes #e0ea8c9
- Merge remote-tracking branch 'origin/downloads' into downloads #e4e605b
- convert to more sensible permission schema #e504c00
- Update on GameDownload #e71e4cf
- reorganisation, cleanup and new nonce protocol #e828bca
- rustix fs feature #e9805a8
- Added manage_go_signal command #ea70ec9
- Drop will no longer crash when the server goes down #eb3311a
- Made all errors type-based #ec2f414
- Added description on how the DownloadManager works #f029cbf
- Using more appropriate logging statements #f183a9d
- remove unnecessary compat code (#20) #f1c8bbf
- Manifests are now being parsed successfully #f28c880
- Removed tests/ #f29e989
- I think that downloads are working. Need to test and set decent file locations now #f388237
- Just debugging tauri's damn Sync command features #f60ca2b
- fixes and patches for merged changes #f6476bc
- Added manage_queue_signal #f64782e
- initial commit #f6cd7c3
- Update .gitlab-ci.yml #fc6bab9
_changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits)
## Release 0.1.0-beta
### Fixes

View file

@ -2,7 +2,13 @@ import { listen } from "@tauri-apps/api/event";
import type { DownloadableMetadata } from "~/types";
export type QueueState = {
queue: Array<{ meta: DownloadableMetadata; status: string; progress: number | null }>;
queue: Array<{
meta: DownloadableMetadata;
status: string;
progress: number | null;
current: number;
max: number;
}>;
status: string;
};
@ -25,4 +31,4 @@ listen("update_queue", (event) => {
listen("update_stats", (event) => {
const stats = useStatsState();
stats.value = event.payload as StatsState;
});
});

View file

@ -12,7 +12,7 @@ export type SerializedGameStatus = [
OptionGameStatus | null
];
const parseStatus = (status: SerializedGameStatus): GameStatus => {
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
console.log(status);
if (status[0]) {
return {

View file

@ -48,9 +48,7 @@ export function initialNavigation(state: Ref<AppState>) {
switch (state.value.status) {
case AppStatus.NotConfigured:
router.push({ path: "/setup" }).then(() => {
console.log("Pushed Setup");
});
router.push({ path: "/setup" });
break;
case AppStatus.SignedOut:
router.push("/auth");

View file

@ -1,7 +1,7 @@
<template>
<div class="flex flex-col bg-zinc-900 overflow-hidden">
<Header class="select-none" />
<div class="grow overflow-y-auto">
<div class="relative grow overflow-y-auto">
<slot />
</div>
</div>

View file

@ -1,7 +1,7 @@
<template>
<div class="flex flex-col bg-zinc-950 overflow-hidden">
<MiniHeader />
<div class="grow overflow-y-auto">
<div class="relative grow overflow-y-auto">
<slot />
</div>
</div>

View file

@ -1,7 +1,7 @@
{
"name": "drop-app",
"private": true,
"version": "0.1.1",
"version": "0.2.0-beta",
"type": "module",
"scripts": {
"build": "nuxt build",

View file

@ -26,7 +26,7 @@
import { XCircleIcon } from "@heroicons/vue/16/solid";
const route = useRoute();
const message = route.query.error ?? "An unknown error occurred.";
const message = route.query.error ?? "An unknown error occurred";
definePageMeta({
layout: "mini",

View file

@ -1,4 +1,7 @@
<template />
<script setup lang="ts">
definePageMeta({
layout: false
})
</script>

View file

@ -1,21 +1,25 @@
<template>
<div class="flex flex-row h-full">
<div class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1">
<div
class="flex-none max-h-full overflow-y-auto w-64 bg-zinc-950 px-2 py-1"
>
<ul class="flex flex-col gap-y-1">
<NuxtLink
v-for="(nav, navIdx) in navigation"
:key="nav.route"
:class="[
'transition-all duration-200 rounded-lg flex items-center py-1.5 px-3',
navIdx === currentNavigationIndex
? 'bg-zinc-800 text-zinc-100'
: 'bg-zinc-900/50 text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-300',
navIdx === currentNavigationIndex
? 'bg-zinc-800 text-zinc-100'
: nav.isInstalled.value
? 'text-zinc-300 hover:bg-zinc-800/90 hover:text-zinc-200'
: 'text-zinc-500 hover:bg-zinc-800/70 hover:text-zinc-300',
]"
:href="nav.route"
>
<div class="flex items-center w-full gap-x-3">
<img
class="h-8 w-10 flex-none object-cover bg-zinc-900"
class="size-6 flex-none object-cover bg-zinc-900 rounded"
:src="icons[navIdx]"
alt=""
/>
@ -27,23 +31,45 @@
</ul>
</div>
<div class="grow overflow-y-auto">
<NuxtPage />
<NuxtPage :libraryDownloadError = "libraryDownloadError" />
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import type { Game, NavigationItem } from "~/types";
import { GameStatusEnum, type Game, type NavigationItem } from "~/types";
const games: Array<Game> = await invoke("fetch_library");
const icons = await Promise.all(games.map((e) => useObject(e.mIconId)));
let libraryDownloadError = false;
const navigation = games.map((e) => {
const item: NavigationItem = {
label: e.mName,
route: `/library/${e.id}`,
prefix: `/library/${e.id}`,
async function calculateGames(): Promise<Game[]> {
try {
return await invoke("fetch_library");
}
catch(e) {
libraryDownloadError = true;
return new Array();
}
}
const rawGames: Array<Game> = await calculateGames();
const games = await Promise.all(rawGames.map((e) => useGame(e.id)));
const icons = await Promise.all(
games.map(({ game, status }) => useObject(game.mIconId))
);
const navigation = games.map(({ game, status }) => {
const isInstalled = computed(
() =>
status.value.type == GameStatusEnum.Installed ||
status.value.type == GameStatusEnum.SetupRequired
);
const item = {
label: game.mName,
route: `/library/${game.id}`,
prefix: `/library/${game.id}`,
isInstalled,
};
return item;
});

View file

@ -242,7 +242,7 @@
"
:loading="installLoading"
type="submit"
class="w-full sm:w-fit"
class="ml-2 w-full sm:w-fit"
>
Install
</LoadingButton>
@ -306,6 +306,7 @@ async function installFlow() {
versionOptions.value = await invoke("fetch_game_verion_options", {
gameId: game.value.id,
});
console.log(versionOptions.value);
installDirs.value = await invoke("fetch_download_dir_stats");
} catch (error) {
installError.value = (error as string).toString();
@ -319,19 +320,19 @@ const installDir = ref(0);
async function install() {
try {
if (!versionOptions.value)
throw new Error("Versions have not been loaded.");
throw new Error("Versions have not been loaded");
installLoading.value = true;
await invoke("download_game", {
gameId: game.value.id,
gameVersion: versionOptions.value[installVersionIndex.value].versionName,
installDir: installDir.value,
});
installLoading.value = false;
installFlowOpen.value = false;
} catch (error) {
installError.value = (error as string).toString();
}
installLoading.value = false;
}
async function launch() {

View file

@ -1,3 +1,9 @@
<script setup lang="ts">
const props = defineProps<{ libraryDownloadError: boolean }>();
</script>
<template>
<div v-if="libraryDownloadError" class="mx-auto pt-10 text-center text-gray-500">
Library Failed to update
</div>
</template>

View file

@ -1,23 +1,37 @@
<template>
<div class="bg-zinc-950 p-4 min-h-full space-y-4">
<div class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900">
<div
class="h-16 overflow-hidden relative rounded-xl flex flex-row border border-zinc-900"
>
<div
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2">
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}</span>
<span v-if="stats.time > 0" class="text-sm">{{ formatTime(stats.time) }} left</span>
class="bg-zinc-900 z-10 w-32 flex flex-col gap-x-2 text-blue-400 font-display items-left justify-center pl-2"
>
<span class="font-semibold">{{ formatKilobytes(stats.speed) }}/s</span>
<span v-if="stats.time > 0" class="text-sm"
>{{ formatTime(stats.time) }} left</span
>
</div>
<div class="absolute inset-0 h-full flex flex-row items-end justify-end">
<div v-for="bar in speedHistory" :style="{ height: `${bar / speedMax * 100}%` }"
class="w-[8px] bg-blue-600/40" />
<div
v-for="bar in speedHistory"
:style="{ height: `${(bar / speedMax) * 100}%` }"
class="w-[8px] bg-blue-600/40"
/>
</div>
</div>
<draggable v-model="queue.queue" @end="onEnd">
<template #item="{ element }: { element: (typeof queue.value.queue)[0] }">
<li v-if="games[element.meta.id]" :key="element.meta.id"
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4">
<li
v-if="games[element.meta.id]"
:key="element.meta.id"
class="mb-4 bg-zinc-900 rounded-lg flex flex-row justify-between gap-x-6 py-5 px-4"
>
<div class="w-full flex items-center max-w-md gap-x-4 relative">
<img class="size-24 flex-none bg-zinc-800 object-cover rounded" :src="games[element.meta.id].cover" alt="" />
<img
class="size-24 flex-none bg-zinc-800 object-cover rounded"
:src="games[element.meta.id].cover"
alt=""
/>
<div class="min-w-0 flex-auto">
<p class="text-xl font-semibold text-zinc-100">
<NuxtLink :href="`/library/${element.meta.id}`" class="">
@ -35,47 +49,68 @@
<p class="text-md text-zinc-500 uppercase font-display font-bold">
{{ element.status }}
</p>
<div v-if="element.progress" class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden">
<div class="h-2 bg-blue-600" :style="{ width: `${element.progress * 100}%` }" />
<div
v-if="element.progress"
class="mt-1 w-96 bg-zinc-800 rounded-lg overflow-hidden"
>
<div
class="h-2 bg-blue-600"
:style="{ width: `${element.progress * 100}%` }"
/>
</div>
<span
class="mt-2 inline-flex items-center gap-x-1 text-zinc-400 text-sm font-display"
><span class="text-zinc-300">{{
formatKilobytes(element.current / 1000)
}}</span>
/
<span class="">{{ formatKilobytes(element.max / 1000) }}</span
><ServerIcon class="size-5"
/></span>
</div>
<button @click="() => cancelGame(element.meta)" class="group">
<XMarkIcon class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
aria-hidden="true" />
<XMarkIcon
class="transition size-8 flex-none text-zinc-600 group-hover:text-zinc-300"
aria-hidden="true"
/>
</button>
</div>
</li>
<p v-else>Loading...</p>
</template>
</draggable>
<div class="text-zinc-600 uppercase font-semibold font-display w-full text-center" v-if="queue.queue.length == 0">
<div
class="text-zinc-600 uppercase font-semibold font-display w-full text-center"
v-if="queue.queue.length == 0"
>
No items in the queue
</div>
</div>
</template>
<script setup lang="ts">
import { XMarkIcon } from "@heroicons/vue/20/solid";
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
import type { DownloadableMetadata, Game, GameStatus } from "~/types";
const windowWidth = ref(window.innerWidth);
window.addEventListener('resize', (event) => {
window.addEventListener("resize", (event) => {
windowWidth.value = window.innerWidth;
})
});
const queue = useQueueState();
const stats = useStatsState();
const speedHistory = useState<Array<number>>(() => []);
const speedHistoryMax = computed(() => windowWidth.value / 8);
const speedMax = computed(() => speedHistory.value.reduce((a, b) => a > b ? a : b) * 1.3);
const speedMax = computed(
() => speedHistory.value.reduce((a, b) => (a > b ? a : b)) * 1.3
);
const previousGameId = ref<string | undefined>();
const games: Ref<{
[key: string]: { game: Game; status: Ref<GameStatus>; cover: string };
}> = ref({});
function resetHistoryGraph() {
speedHistory.value = [];
stats.value = { time: 0, speed: 0 };
@ -97,8 +132,7 @@ function checkReset(v: QueueState) {
return;
}
// If it's a different game now
if (currentGame != previousGameId.value
) {
if (currentGame != previousGameId.value) {
previousGameId.value = currentGame;
resetHistoryGraph();
return;
@ -115,10 +149,12 @@ watch(stats, (v) => {
speedHistory.value.splice(0, 1);
}
checkReset(queue.value);
})
});
function loadGamesForQueue(v: typeof queue.value) {
for (const { meta: { id } } of v.queue) {
for (const {
meta: { id },
} of v.queue) {
if (games.value[id]) return;
(async () => {
const gameData = await useGame(id);
@ -131,7 +167,7 @@ function loadGamesForQueue(v: typeof queue.value) {
loadGamesForQueue(queue.value);
async function onEnd(event: { oldIndex: number; newIndex: number }) {
await invoke("move_game_in_queue", {
await invoke("move_download_in_queue", {
oldIndex: event.oldIndex,
newIndex: event.newIndex,
});
@ -152,7 +188,7 @@ function formatKilobytes(bytes: number): string {
unitIndex++;
}
return `${value.toFixed(1)} ${units[unitIndex]}/s`;
return `${value.toFixed(1)} ${units[unitIndex]}`;
}
function formatTime(seconds: number): string {
@ -162,7 +198,7 @@ function formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes}m ${Math.round(seconds % 60)}s`
return `${minutes}m ${Math.round(seconds % 60)}s`;
}
const hours = Math.floor(minutes / 60);

View file

@ -44,8 +44,45 @@ import {
import type { Component } from "vue";
import type { NavigationItem } from "~/types";
import { platform } from '@tauri-apps/plugin-os';
import { invoke } from "@tauri-apps/api/core";
const navigation: Array<NavigationItem & { icon: Component }> = [
const systemData = await invoke<{
clientId: string;
baseUrl: string;
dataDir: string;
logLevel: string;
}>("fetch_system_data");
const isDebugMode = ref(systemData.logLevel.toLowerCase() === "debug");
const debugRevealed = ref(false);
// Track shift key state and debug reveal
onMounted(() => {
window.addEventListener('keydown', (e) => {
if (e.key === 'Shift') {
isDebugMode.value = true;
debugRevealed.value = true;
}
});
window.addEventListener('keyup', (e) => {
if (e.key === 'Shift') {
isDebugMode.value = debugRevealed.value || systemData.logLevel.toLowerCase() === "debug";
}
});
// Reset debug reveal when leaving the settings page
const router = useRouter();
router.beforeEach((to) => {
if (!to.path.startsWith('/settings')) {
debugRevealed.value = false;
isDebugMode.value = systemData.logLevel.toLowerCase() === "debug";
}
});
});
// Make navigation reactive by wrapping in computed
const navigation = computed(() => [
{
label: "Home",
route: "/settings",
@ -53,7 +90,7 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
icon: HomeIcon,
},
{
label: "Interface",
label: "Interface",
route: "/settings/interface",
prefix: "/settings/interface",
icon: RectangleGroupIcon,
@ -64,20 +101,21 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
prefix: "/settings/downloads",
icon: ArrowDownTrayIcon,
},
{
...(isDebugMode.value ? [{
label: "Debug Info",
route: "/settings/debug",
route: "/settings/debug",
prefix: "/settings/debug",
icon: BugAntIcon,
},
];
}] : []),
]);
const currentPlatform = platform();
switch (currentPlatform) {
case "linux":
navigation.splice(2, 0, { label: "Compatibility", route: "/settings/compatibility", prefix: "/settings/compatibility", icon: CubeIcon });
break;
}
const currentPageIndex = useCurrentNavigationIndex(navigation);
// Use .value to unwrap the computed ref
const currentPageIndex = useCurrentNavigationIndex(navigation.value);
// Watch for navigation changes and update currentPageIndex
watch(navigation, (newNav) => {
currentPageIndex.value = useCurrentNavigationIndex(newNav).value;
});
</script>

View file

@ -1,3 +0,0 @@
<template>
</template>

View file

@ -58,26 +58,6 @@
</p>
</div>
<div v-if="compatInfo">
<div class="flex items-center gap-x-3">
<CubeIcon class="h-5 w-5 text-zinc-400" />
<h3 class="text-sm font-medium leading-6 text-zinc-100">
Compatibility Settings
</h3>
</div>
<div class="mt-2 ml-8 space-y-1">
<p class="text-sm text-zinc-400 font-mono">
Enabled: {{ compatInfo.enabled ? "Yes" : "No" }}
</p>
<p class="text-sm text-zinc-400 font-mono">
Runner: {{ compatInfo.runner || "Not configured" }}
</p>
<p class="text-sm text-zinc-400 font-mono">
Prefix: {{ compatInfo.prefix || "Not configured" }}
</p>
</div>
</div>
<div class="pt-6 flex gap-x-4">
<button
@click="() => openDataDir()"
@ -119,11 +99,6 @@ const clientId = ref<string | null>(null);
const platformInfo = ref("Loading...");
const baseUrl = ref<string | null>(null);
const dataDir = ref<string | null>(null);
const compatInfo = ref<{
enabled: boolean;
runner: string | null;
prefix: string | null;
} | null>(null);
const systemData = await invoke<{
clientId: string;

View file

@ -59,6 +59,54 @@
</div>
</li>
</ul>
<div class="border-t border-zinc-600 py-6">
<h3 class="text-base font-display font-semibold text-zinc-100">
Download Settings
</h3>
<p class="mt-1 text-sm text-zinc-400 max-w-xl">
Configure how Drop downloads games and other content.
</p>
<div class="mt-6 max-w-xl">
<label for="threads" class="block text-sm font-medium text-zinc-100">
Maximum Download Threads
</label>
<div class="mt-2">
<input
type="number"
name="threads"
id="threads"
min="1"
max="32"
v-model="downloadThreads"
@keypress="validateNumberInput"
@paste="validatePaste"
class="block w-full rounded-md border-0 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
<p class="mt-2 text-sm text-zinc-400">
The maximum number of concurrent download threads. Higher values may
download faster but use more system resources. Default is 4.
</p>
</div>
<div class="mt-6">
<button
type="button"
@click="saveDownloadThreads"
:disabled="saveState.loading"
:class="[
'inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-colors duration-300',
saveState.success
? 'bg-green-600 hover:bg-green-500 focus-visible:outline-green-600'
: 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600',
'disabled:bg-blue-600/50 disabled:cursor-not-allowed'
]"
>
{{ saveState.success ? 'Saved' : 'Save Changes' }}
</button>
</div>
</div>
</div>
<TransitionRoot as="template" :show="open">
<Dialog class="relative z-50" @close="open = false">
@ -172,6 +220,7 @@ import {
} from "@headlessui/vue";
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
import { invoke } from "@tauri-apps/api/core";
import { type Settings } from "~/types";
const open = ref(false);
const currentDirectory = ref<string | undefined>(undefined);
@ -180,6 +229,14 @@ const createDirectoryLoading = ref(false);
const dirs = ref<Array<string>>([]);
const settings = await invoke<Settings>("fetch_settings");
const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
const saveState = reactive({
loading: false,
success: false
});
async function updateDirs() {
const newDirs = await invoke<Array<string>>("fetch_download_dir_stats");
dirs.value = newDirs;
@ -213,7 +270,7 @@ async function submitDirectory() {
try {
error.value = undefined;
if (!currentDirectory.value)
throw new Error("Please select a directory first.");
throw new Error("Please select a directory first");
createDirectoryLoading.value = true;
// Add directory
@ -235,4 +292,42 @@ async function deleteDirectory(index: number) {
await invoke("delete_download_dir", { index });
await updateDirs();
}
async function saveDownloadThreads() {
try {
saveState.loading = true;
await invoke("update_settings", {
newSettings: { maxDownloadThreads: downloadThreads.value },
});
// Show success state
saveState.success = true;
// Reset back to normal state after 2 seconds
setTimeout(() => {
saveState.success = false;
}, 2000);
} catch (error) {
console.error('Failed to save settings:', error);
} finally {
saveState.loading = false;
}
}
function validateNumberInput(event: KeyboardEvent) {
// Allow only numbers and basic control keys
if (!/^\d$/.test(event.key) &&
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
event.preventDefault();
}
}
function validatePaste(event: ClipboardEvent) {
// Prevent paste if content contains non-numeric characters
const pastedData = event.clipboardData?.getData('text');
if (pastedData && !/^\d+$/.test(pastedData)) {
event.preventDefault();
}
}
</script>

View file

@ -1,7 +1,7 @@
export default defineNuxtPlugin((nuxtApp) => {
// Also possible
nuxtApp.hook("vue:error", (error, instance, info) => {
console.log(error);
console.error(error, info);
const router = useRouter();
router.replace(`/error`);
});

90
src-tauri/Cargo.lock generated
View file

@ -255,6 +255,12 @@ dependencies = [
"system-deps",
]
[[package]]
name = "atomic-instant-full"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6541700e074cda41b1c6f98c2cae6cde819967bf142078f069cad85387cdbe"
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -1000,8 +1006,9 @@ dependencies = [
[[package]]
name = "drop-app"
version = "0.1.0"
version = "0.2.0-beta-prerelease-1"
dependencies = [
"atomic-instant-full",
"boxcar",
"chrono",
"directories",
@ -1011,6 +1018,7 @@ dependencies = [
"log4rs",
"md5",
"openssl",
"parking_lot 0.12.3",
"rayon",
"reqwest",
"rustbreak",
@ -1020,6 +1028,7 @@ dependencies = [
"serde_json",
"serde_with",
"shared_child",
"slice-deque",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
@ -1028,6 +1037,7 @@ dependencies = [
"tauri-plugin-os",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"throttle_my_fn",
"tokio",
"umu-wrapper-lib",
"url",
@ -2341,7 +2351,7 @@ dependencies = [
"log",
"log-mdc",
"once_cell",
"parking_lot",
"parking_lot 0.12.3",
"rand 0.8.5",
"serde",
"serde-value",
@ -2359,6 +2369,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mach"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -2958,6 +2977,17 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core 0.8.6",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
@ -2965,7 +2995,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
"parking_lot_core 0.9.10",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall 0.2.16",
"smallvec",
"winapi",
]
[[package]]
@ -2976,7 +3020,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"redox_syscall 0.5.8",
"smallvec",
"windows-targets 0.52.6",
]
@ -3411,6 +3455,15 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.8"
@ -3998,6 +4051,17 @@ dependencies = [
"autocfg",
]
[[package]]
name = "slice-deque"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25"
dependencies = [
"libc",
"mach",
"winapi",
]
[[package]]
name = "smallvec"
version = "1.13.2"
@ -4030,7 +4094,7 @@ dependencies = [
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle",
"redox_syscall",
"redox_syscall 0.5.8",
"wasm-bindgen",
"web-sys",
"windows-sys 0.59.0",
@ -4088,7 +4152,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
dependencies = [
"new_debug_unreachable",
"once_cell",
"parking_lot",
"parking_lot 0.12.3",
"phf_shared 0.10.0",
"precomputed-hash",
"serde",
@ -4241,7 +4305,7 @@ dependencies = [
"ndk-sys",
"objc",
"once_cell",
"parking_lot",
"parking_lot 0.12.3",
"raw-window-handle",
"scopeguard",
"tao-macros",
@ -4702,6 +4766,18 @@ dependencies = [
"winapi",
]
[[package]]
name = "throttle_my_fn"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "482c185e5675626c9a130b3a8f362c322a239338c882f745a1d9a85838b987f0"
dependencies = [
"parking_lot 0.11.2",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "time"
version = "0.3.37"

View file

@ -1,6 +1,6 @@
[package]
name = "drop-app"
version = "0.1.0"
version = "0.2.0-beta-prerelease-1"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"]
edition = "2021"
@ -46,6 +46,10 @@ umu-wrapper-lib = "0.1.0"
tauri-plugin-autostart = "2.0.0"
shared_child = "1.0.1"
serde_with = "3.12.0"
slice-deque = "0.3.0"
throttle_my_fn = "0.2.6"
parking_lot = "0.12.3"
atomic-instant-full = "0.1.0"
[dependencies.tauri]
version = "2.1.1"

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View file

@ -1,69 +1,76 @@
use log::info;
use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt;
use crate::DB;
#[tauri::command]
pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
let manager = app.autolaunch();
if enabled {
manager.enable().map_err(|e| e.to_string())?;
info!("Enabled autostart");
} else {
manager.disable().map_err(|e| e.to_string())?;
info!("Disabled autostart");
}
// Store the state in DB
let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?;
db_handle.settings.autostart = enabled;
drop(db_handle);
DB.save().map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn get_autostart_enabled(app: AppHandle) -> Result<bool, String> {
// First check DB state
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
let db_state = db_handle.settings.autostart;
drop(db_handle);
// Get actual system state
let manager = app.autolaunch();
let system_state = manager.is_enabled().map_err(|e| e.to_string())?;
// If they don't match, sync to DB state
if db_state != system_state {
if db_state {
manager.enable().map_err(|e| e.to_string())?;
} else {
manager.disable().map_err(|e| e.to_string())?;
}
}
Ok(db_state)
}
// New function to sync state on startup
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
let should_be_enabled = db_handle.settings.autostart;
drop(db_handle);
let manager = app.autolaunch();
let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
if current_state != should_be_enabled {
if should_be_enabled {
manager.enable().map_err(|e| e.to_string())?;
info!("Synced autostart: enabled");
} else {
manager.disable().map_err(|e| e.to_string())?;
info!("Synced autostart: disabled");
}
}
Ok(())
}
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
use log::debug;
use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt;
pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> {
let manager = app.autolaunch();
if enabled {
manager.enable().map_err(|e| e.to_string())?;
debug!("enabled autostart");
} else {
manager.disable().map_err(|e| e.to_string())?;
debug!("eisabled autostart");
}
// Store the state in DB
let mut db_handle = borrow_db_mut_checked();
db_handle.settings.autostart = enabled;
drop(db_handle);
save_db();
Ok(())
}
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
// First check DB state
let db_handle = borrow_db_checked();
let db_state = db_handle.settings.autostart;
drop(db_handle);
// Get actual system state
let manager = app.autolaunch();
let system_state = manager.is_enabled()?;
// If they don't match, sync to DB state
if db_state != system_state {
if db_state {
manager.enable()?;
} else {
manager.disable()?;
}
}
Ok(db_state)
}
// New function to sync state on startup
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
let db_handle = borrow_db_checked();
let should_be_enabled = db_handle.settings.autostart;
drop(db_handle);
let manager = app.autolaunch();
let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
if current_state != should_be_enabled {
if should_be_enabled {
manager.enable().map_err(|e| e.to_string())?;
debug!("synced autostart: enabled");
} else {
manager.disable().map_err(|e| e.to_string())?;
debug!("synced autostart: disabled");
}
}
Ok(())
}
#[tauri::command]
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
toggle_autostart_logic(app, enabled)
}
#[tauri::command]
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
get_autostart_enabled_logic(app)
}

View file

@ -1,15 +1,23 @@
use log::info;
use log::{debug, error};
use tauri::AppHandle;
use crate::AppState;
#[tauri::command]
pub fn quit(app: tauri::AppHandle) {
cleanup_and_exit(&app);
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
cleanup_and_exit(&app, &state);
}
pub fn cleanup_and_exit(app: &AppHandle) {
info!("exiting drop application...");
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
debug!("cleaning up and exiting application");
let download_manager = state.lock().unwrap().download_manager.clone();
match download_manager.ensure_terminated() {
Ok(res) => match res {
Ok(_) => debug!("download manager terminated correctly"),
Err(_) => error!("download manager failed to terminate correctly"),
},
Err(e) => panic!("{:?}", e),
}
app.exit(0);
}

11
src-tauri/src/commands.rs Normal file
View file

@ -0,0 +1,11 @@
use crate::AppState;
#[tauri::command]
pub fn fetch_state(
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
) -> Result<String, String> {
let guard = state.lock().unwrap();
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
drop(guard);
Ok(cloned_state)
}

View file

@ -0,0 +1,92 @@
use std::{
fs::create_dir_all,
io::{Error, ErrorKind},
path::{Path, PathBuf},
};
use serde_json::Value;
use crate::{
database::{db::borrow_db_mut_checked, settings::Settings},
download_manager::internal_error::InternalError,
};
use super::{
db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
debug::SystemData,
};
// Will, in future, return disk/remaining size
// Just returns the directories that have been set up
#[tauri::command]
pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
let lock = borrow_db_checked();
lock.applications.install_dirs.clone()
}
#[tauri::command]
pub fn delete_download_dir(index: usize) {
let mut lock = borrow_db_mut_checked();
lock.applications.install_dirs.remove(index);
drop(lock);
save_db();
}
#[tauri::command]
pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> {
// Check the new directory is all good
let new_dir_path = Path::new(&new_dir);
if new_dir_path.exists() {
let dir_contents = new_dir_path.read_dir()?;
if dir_contents.count() != 0 {
return Err(Error::new(
ErrorKind::DirectoryNotEmpty,
"Selected directory cannot contain any existing files",
)
.into());
}
} else {
create_dir_all(new_dir_path)?;
}
// Add it to the dictionary
let mut lock = borrow_db_mut_checked();
if lock.applications.install_dirs.contains(&new_dir) {
return Err(Error::new(
ErrorKind::AlreadyExists,
"Selected directory already exists in database",
)
.into());
}
lock.applications.install_dirs.push(new_dir);
drop(lock);
save_db();
Ok(())
}
#[tauri::command]
pub fn update_settings(new_settings: Value) {
let mut db_lock = borrow_db_mut_checked();
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
for (key, value) in new_settings.as_object().unwrap() {
current_settings[key] = value.clone();
}
let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
db_lock.settings = new_settings;
println!("new Settings: {:?}", db_lock.settings);
}
#[tauri::command]
pub fn fetch_settings() -> Settings {
borrow_db_checked().settings.clone()
}
#[tauri::command]
pub fn fetch_system_data() -> SystemData {
let db_handle = borrow_db_checked();
SystemData::new(
db_handle.auth.as_ref().unwrap().client_id.clone(),
db_handle.base_url.clone(),
DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
)
}

View file

@ -1,20 +1,27 @@
use std::{
collections::HashMap,
fs::{self, create_dir_all},
hash::Hash,
path::{Path, PathBuf},
sync::{Arc, LazyLock, Mutex, RwLockWriteGuard}, time::{Instant, SystemTime, UNIX_EPOCH},
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
};
use chrono::Utc;
use directories::BaseDirs;
use log::debug;
use log::{debug, error, info};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_with::serde_as;
use tauri::AppHandle;
use url::Url;
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, games::{library::push_game_update, state::GameStatusManager}, process::process_manager::Platform, DB};
use crate::{
database::settings::Settings,
download_manager::downloadable_metadata::DownloadableMetadata,
games::{library::push_game_update, state::GameStatusManager},
process::process_manager::Platform,
DB,
};
#[derive(serde::Serialize, Clone, Deserialize)]
pub struct DatabaseAuth {
@ -50,18 +57,30 @@ pub enum ApplicationTransientStatus {
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GameVersion {
pub version_index: usize,
pub game_id: String,
pub version_name: String,
pub launch_command: String,
pub setup_command: String,
pub platform: Platform,
pub launch_command: String,
pub launch_args: Vec<String>,
pub setup_command: String,
pub setup_args: Vec<String>,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
pub umu_id_override: Option<String>,
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize)]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct DatabaseApplications {
pub install_dirs: Vec<String>,
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
@ -71,29 +90,34 @@ pub struct DatabaseApplications {
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct Settings {
pub autostart: bool,
// ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
// ... other settings defaults ...
}
}
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
pub prev_database: Option<PathBuf>
pub prev_database: Option<PathBuf>,
}
impl Database {
fn new<T: Into<PathBuf>>(games_base_dir: T, prev_database: Option<PathBuf>) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings {
autostart: false,
max_download_threads: 4,
},
}
}
}
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
@ -127,35 +151,20 @@ impl DatabaseImpls for DatabaseInterface {
let games_base_dir = data_root_dir.join("games");
let logs_root_dir = data_root_dir.join("logs");
debug!("Creating data directory at {:?}", data_root_dir);
debug!("creating data directory at {:?}", data_root_dir);
create_dir_all(data_root_dir.clone()).unwrap();
debug!("Creating games directory");
create_dir_all(games_base_dir.clone()).unwrap();
debug!("Creating logs directory");
create_dir_all(logs_root_dir.clone()).unwrap();
let exists = fs::exists(db_path.clone()).unwrap();
match exists {
true =>
match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir),
},
true => match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir),
},
false => {
let default = Database {
settings: Settings::default(),
auth: None,
base_url: "".to_string(),
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
game_statuses: HashMap::new(),
transient_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
},
prev_database: None,
};
let default = Database::new(games_base_dir, None);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
@ -176,103 +185,72 @@ impl DatabaseImpls for DatabaseInterface {
}
}
#[tauri::command]
pub fn add_download_dir(new_dir: String) -> Result<(), String> {
// Check the new directory is all good
let new_dir_path = Path::new(&new_dir);
if new_dir_path.exists() {
let metadata = new_dir_path
.metadata()
.map_err(|e| format!("Unable to access file or directory: {}", e))?;
if !metadata.is_dir() {
return Err("Invalid path: not a directory".to_string());
}
let dir_contents = new_dir_path
.read_dir()
.map_err(|e| format!("Unable to check directory contents: {}", e))?;
if dir_contents.count() != 0 {
return Err("Directory is not empty".to_string());
}
} else {
create_dir_all(new_dir_path)
.map_err(|e| format!("Unable to create directories to path: {}", e))?;
}
// Add it to the dictionary
let mut lock = DB.borrow_data_mut().unwrap();
if lock.applications.install_dirs.contains(&new_dir) {
return Err("Download directory already used".to_string());
}
lock.applications.install_dirs.push(new_dir);
drop(lock);
DB.save().unwrap();
Ok(())
}
#[tauri::command]
pub fn delete_download_dir(index: usize) -> Result<(), String> {
let mut lock = DB.borrow_data_mut().unwrap();
lock.applications.install_dirs.remove(index);
drop(lock);
DB.save().unwrap();
Ok(())
}
// Will, in future, return disk/remaining size
// Just returns the directories that have been set up
#[tauri::command]
pub fn fetch_download_dir_stats() -> Result<Vec<String>, String> {
let lock = DB.borrow_data().unwrap();
let directories = lock.applications.install_dirs.clone();
drop(lock);
Ok(directories)
}
pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &DownloadableMetadata)>(
app_handle: &AppHandle,
meta: DownloadableMetadata,
setter: F,
) {
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
setter(&mut db_handle, &meta);
drop(db_handle);
DB.save().unwrap();
save_db();
let status = GameStatusManager::fetch_state(&meta.id);
push_game_update(app_handle, &meta, status);
push_game_update(app_handle, &meta.id, status);
}
// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
fn handle_invalid_database(_e: RustbreakError, db_path: PathBuf, games_base_dir: PathBuf) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
let new_path = {
fn handle_invalid_database(
_e: RustbreakError,
db_path: PathBuf,
games_base_dir: PathBuf,
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
let new_path = {
let time = Utc::now().timestamp();
let mut base = db_path.clone().into_os_string();
base.push(".");
base.push(time.to_string());
let mut base = db_path.clone();
base.set_file_name(format!("drop.db.backup-{}", time));
base
};
fs::copy(&db_path, &new_path).unwrap();
info!(
"old database stored at: {}",
new_path.to_string_lossy().to_string()
);
fs::rename(&db_path, &new_path).unwrap();
let db = Database {
auth: None,
base_url: "".to_string(),
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
game_statuses: HashMap::new(),
transient_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
},
prev_database: Some(new_path.into()),
settings: Settings { autostart: false },
};
let db = Database::new(
games_base_dir.into_os_string().into_string().unwrap(),
Some(new_path),
);
PathDatabase::create_at_path(db_path, db)
.expect("Database could not be created")
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
}
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
match DB.borrow_data() {
Ok(data) => data,
Err(e) => {
error!("database borrow failed with error {}", e);
panic!("database borrow failed with error {}", e);
}
}
}
}
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
match DB.borrow_data_mut() {
Ok(data) => data,
Err(e) => {
error!("database borrow mut failed with error {}", e);
panic!("database borrow mut failed with error {}", e);
}
}
}
pub fn save_db() {
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
}
}
}

View file

@ -0,0 +1,21 @@
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemData {
client_id: String,
base_url: String,
data_dir: String,
log_level: String,
}
impl SystemData {
pub fn new(client_id: String, base_url: String, data_dir: String, log_level: String) -> Self {
Self {
client_id,
base_url,
data_dir,
log_level,
}
}
}

View file

@ -0,0 +1,4 @@
pub mod commands;
pub mod db;
pub mod debug;
pub mod settings;

View file

@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
// ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
}
}
}
// Ideally use pointers instead of a macro to assign the settings
// fn deserialize_into<T>(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
// where T: for<'a> Deserialize<'a>
// {
// *t = serde_json::from_value(v)?;
// Ok(())
// }

View file

@ -1,23 +0,0 @@
use crate::{DATA_ROOT_DIR, DB};
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemData {
client_id: String,
base_url: String,
data_dir: String,
}
#[tauri::command]
pub fn fetch_system_data() -> Result<SystemData, String> {
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
let system_data = SystemData {
client_id: db_handle.auth.as_ref().unwrap().client_id.clone(),
base_url: db_handle.base_url.clone(),
data_dir: DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
};
drop(db_handle);
Ok(system_data)
}

View file

@ -0,0 +1,31 @@
use std::sync::Mutex;
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState};
#[tauri::command]
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.pause_downloads()
}
#[tauri::command]
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.resume_downloads()
}
#[tauri::command]
pub fn move_download_in_queue(
state: tauri::State<'_, Mutex<AppState>>,
old_index: usize,
new_index: usize,
) {
state
.lock()
.unwrap()
.download_manager
.rearrange(old_index, new_index)
}
#[tauri::command]
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
state.lock().unwrap().download_manager.cancel(meta)
}

View file

@ -4,16 +4,21 @@ use std::{
fmt::Debug,
sync::{
mpsc::{SendError, Sender},
Arc, Mutex, MutexGuard,
Mutex, MutexGuard,
},
thread::JoinHandle,
};
use log::info;
use log::{debug, info};
use serde::Serialize;
use crate::error::application_download_error::ApplicationDownloadError;
use super::{application_download_error::ApplicationDownloadError, download_manager_builder::{CurrentProgressObject, DownloadAgent}, downloadable_metadata::DownloadableMetadata, queue::Queue};
use super::{
download_manager_builder::{CurrentProgressObject, DownloadAgent},
downloadable_metadata::DownloadableMetadata,
queue::Queue,
};
pub enum DownloadManagerSignal {
/// Resumes (or starts) the DownloadManager
@ -79,7 +84,7 @@ pub enum DownloadStatus {
/// which provides raw access to the underlying queue.
/// THIS EDITING IS BLOCKING!!!
pub struct DownloadManager {
terminator: JoinHandle<Result<(), ()>>,
terminator: Mutex<Option<JoinHandle<Result<(), ()>>>>,
download_queue: Queue,
progress: CurrentProgressObject,
command_sender: Sender<DownloadManagerSignal>,
@ -94,7 +99,7 @@ impl DownloadManager {
command_sender: Sender<DownloadManagerSignal>,
) -> Self {
Self {
terminator,
terminator: Mutex::new(Some(terminator)),
download_queue,
progress,
command_sender,
@ -103,10 +108,11 @@ impl DownloadManager {
pub fn queue_download(
&self,
download: DownloadAgent
download: DownloadAgent,
) -> Result<(), SendError<DownloadManagerSignal>> {
info!("Adding download id {:?}", download.metadata());
self.command_sender.send(DownloadManagerSignal::Queue(download))?;
info!("creating download with meta {:?}", download.metadata());
self.command_sender
.send(DownloadManagerSignal::Queue(download))?;
self.command_sender.send(DownloadManagerSignal::Go)
}
pub fn edit(&self) -> MutexGuard<'_, VecDeque<DownloadableMetadata>> {
@ -145,12 +151,14 @@ impl DownloadManager {
.unwrap();
}
info!("moving {} to {}", current_index, new_index);
debug!(
"moving download at index {} to index {}",
current_index, new_index
);
let mut queue = self.edit();
let to_move = queue.remove(current_index).unwrap();
queue.insert(new_index, to_move);
info!("new queue: {:?}", queue);
drop(queue);
if needs_pause {
@ -159,6 +167,10 @@ impl DownloadManager {
self.command_sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
self.command_sender
.send(DownloadManagerSignal::Go)
.unwrap();
}
pub fn pause_downloads(&self) {
self.command_sender
@ -168,11 +180,12 @@ impl DownloadManager {
pub fn resume_downloads(&self) {
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
}
pub fn ensure_terminated(self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
pub fn ensure_terminated(&self) -> Result<Result<(), ()>, Box<dyn Any + Send>> {
self.command_sender
.send(DownloadManagerSignal::Finish)
.unwrap();
self.terminator.join()
let terminator = self.terminator.lock().unwrap().take();
terminator.unwrap().join()
}
pub fn uninstall_application(&self, meta: DownloadableMetadata) {
self.command_sender

View file

@ -1,19 +1,28 @@
use std::{
collections::HashMap,
fs::remove_dir_all,
sync::{
mpsc::{channel, Receiver, Sender},
Arc, Mutex, RwLockWriteGuard,
Arc, Mutex,
},
thread::{spawn, JoinHandle},
};
use log::{error, info};
use log::{debug, error, info, warn};
use tauri::{AppHandle, Emitter};
use crate::games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent};
use crate::{
error::application_download_error::ApplicationDownloadError,
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
};
use super::{application_download_error::ApplicationDownloadError, download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject, queue::Queue};
use super::{
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
downloadable::Downloadable,
downloadable_metadata::DownloadableMetadata,
progress_object::ProgressObject,
queue::Queue,
};
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
pub type CurrentProgressObject = Arc<Mutex<Option<Arc<ProgressObject>>>>;
@ -47,7 +56,7 @@ whichever download queue order is required.
+----------------------------------------------------------------------------+
This download queue does not actually own any of the DownloadAgents. It is
simply a id-based reference system. The actual Agents are stored in the
simply an id-based reference system. The actual Agents are stored in the
download_agent_registry HashMap, as ordering is no issue here. This is why
appending or removing from the download_queue must be done via signals.
@ -115,7 +124,6 @@ impl DownloadManagerBuilder {
let mut download_thread_lock = self.current_download_thread.lock().unwrap();
*download_thread_lock = None;
drop(download_thread_lock);
}
fn stop_and_wait_current_download(&self) {
@ -130,7 +138,6 @@ impl DownloadManagerBuilder {
}
}
fn manage_queue(mut self) -> Result<(), ()> {
loop {
let signal = match self.command_receiver.recv() {
@ -172,13 +179,13 @@ impl DownloadManagerBuilder {
}
}
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
info!("Got signal Queue");
debug!("got signal Queue");
let meta = download_agent.metadata();
info!("Meta: {:?}", meta);
debug!("queue metadata: {:?}", meta);
if self.download_queue.exists(meta.clone()) {
info!("Download with same ID already exists");
warn!("download with same ID already exists");
return;
}
@ -186,27 +193,37 @@ impl DownloadManagerBuilder {
self.download_queue.append(meta.clone());
self.download_agent_registry.insert(meta, download_agent);
self.sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
self.sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}
fn manage_go_signal(&mut self) {
info!("Got signal Go");
if self.download_agent_registry.is_empty() {
info!("Download agent registry: {:?}", self.download_agent_registry.len());
return;
}
if self.current_download_agent.is_some() {
info!("Current download agent: {:?}", self.current_download_agent.as_ref().unwrap().metadata());
debug!("got signal Go");
if self.download_agent_registry.is_empty() {
debug!(
"Download agent registry: {:?}",
self.download_agent_registry.len()
);
return;
}
info!("Current download queue: {:?}", self.download_queue.read());
if self.current_download_agent.is_some() {
if self.download_queue.read().front().unwrap() == &self.current_download_agent.as_ref().unwrap().metadata() {
debug!(
"Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata()
);
return;
}
}
debug!("current download queue: {:?}", self.download_queue.read());
// Should always be Some if the above two statements keep going
let agent_data = self.download_queue.read().front().unwrap().clone();
info!("Starting download for {:?}", agent_data);
info!("starting download for {:?}", agent_data);
let download_agent = self
.download_agent_registry
@ -226,17 +243,21 @@ impl DownloadManagerBuilder {
match download_agent.download(&app_handle) {
// Ok(true) is for completed and exited properly
Ok(true) => {
debug!("download {:?} has completed", download_agent.metadata());
download_agent.on_complete(&app_handle);
sender.send(DownloadManagerSignal::Completed(download_agent.metadata())).unwrap();
},
sender
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
.unwrap();
}
// Ok(false) is for incomplete but exited properly
Ok(false) => {
download_agent.on_incomplete(&app_handle);
},
}
Err(e) => {
error!("download {:?} has error {}", download_agent.metadata(), &e);
download_agent.on_error(&app_handle, e.clone());
sender.send(DownloadManagerSignal::Error(e)).unwrap();
},
}
}
sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
}));
@ -246,7 +267,7 @@ impl DownloadManagerBuilder {
active_control_flag.set(DownloadThreadControlFlag::Go);
}
fn manage_stop_signal(&mut self) {
info!("Got signal Stop");
debug!("got signal Stop");
if let Some(active_control_flag) = self.active_control_flag.clone() {
self.set_status(DownloadManagerStatus::Paused);
@ -254,10 +275,9 @@ impl DownloadManagerBuilder {
}
}
fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
info!("Got signal Completed");
debug!("got signal Completed");
if let Some(interface) = &self.current_download_agent {
if interface.metadata() == meta {
info!("Popping consumed data");
self.remove_and_cleanup_front_download(&meta);
}
}
@ -265,18 +285,17 @@ impl DownloadManagerBuilder {
self.sender.send(DownloadManagerSignal::Go).unwrap();
}
fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
info!("Got signal Error");
let current_agent = self.current_download_agent.clone().unwrap();
current_agent.on_error(&self.app_handle, error.clone());
self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(&current_agent.metadata());
debug!("got signal Error");
if let Some(current_agent) = self.current_download_agent.clone() {
current_agent.on_error(&self.app_handle, error.clone());
self.stop_and_wait_current_download();
self.remove_and_cleanup_front_download(&current_agent.metadata());
}
self.set_status(DownloadManagerStatus::Error(error));
}
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
info!("Got signal Cancel");
debug!("got signal Cancel");
if let Some(current_download) = &self.current_download_agent {
if &current_download.metadata() == meta {
@ -287,33 +306,34 @@ impl DownloadManagerBuilder {
self.download_queue.pop_front();
self.cleanup_current_download();
info!("Current donwload queue: {:?}", self.download_queue.read());
debug!("current download queue: {:?}", self.download_queue.read());
}
// TODO: Collapse these two into a single if statement somehow
else {
if let Some(download_agent) = self.download_agent_registry.get(meta) {
info!("Object exists in registry");
let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle);
let _ = self.download_queue.edit().remove(index).unwrap();
let removed = self.download_agent_registry.remove(meta);
info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read());
}
}
}
}
else {
if let Some(download_agent) = self.download_agent_registry.get(meta) {
info!("Object exists in registry");
else if let Some(download_agent) = self.download_agent_registry.get(meta) {
let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle);
let _ = self.download_queue.edit().remove(index).unwrap();
let removed = self.download_agent_registry.remove(meta);
info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read());
debug!(
"removed {:?} from queue {:?}",
removed.map(|x| x.metadata()),
self.download_queue.read()
);
}
}
} else if let Some(download_agent) = self.download_agent_registry.get(meta) {
let index = self.download_queue.get_by_meta(meta);
if let Some(index) = index {
download_agent.on_cancelled(&self.app_handle);
let _ = self.download_queue.edit().remove(index).unwrap();
let removed = self.download_agent_registry.remove(meta);
debug!(
"removed {:?} from queue {:?}",
removed.map(|x| x.metadata()),
self.download_queue.read()
);
}
}
self.push_ui_queue_update();
}
@ -326,18 +346,19 @@ impl DownloadManagerBuilder {
let queue = &self.download_queue.read();
let queue_objs = queue
.iter()
.map(|(key)| {
.map(|key| {
let val = self.download_agent_registry.get(key).unwrap();
QueueUpdateEventQueueData {
meta: DownloadableMetadata::clone(&key),
meta: DownloadableMetadata::clone(key),
status: val.status(),
progress: val.progress().get_progress()
}})
progress: val.progress().get_progress(),
current: val.progress().sum(),
max: val.progress().get_max(),
}
})
.collect();
let event_data = QueueUpdateEvent {
queue: queue_objs,
};
let event_data = QueueUpdateEvent { queue: queue_objs };
self.app_handle.emit("update_queue", event_data).unwrap();
}
}
}

View file

@ -1,9 +1,12 @@
use std::{fmt::{self, Debug}, sync::{mpsc::Sender, Arc}};
use std::sync::Arc;
use tauri::AppHandle;
use crate::error::application_download_error::ApplicationDownloadError;
use super::{
application_download_error::ApplicationDownloadError, download_manager::{DownloadManagerSignal, DownloadStatus}, download_thread_control_flag::DownloadThreadControl, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject
download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl,
downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject,
};
pub trait Downloadable: Send + Sync {
@ -17,4 +20,4 @@ pub trait Downloadable: Send + Sync {
fn on_complete(&self, app_handle: &AppHandle);
fn on_incomplete(&self, app_handle: &AppHandle);
fn on_cancelled(&self, app_handle: &AppHandle);
}
}

View file

@ -5,7 +5,7 @@ pub enum DownloadType {
Game,
Tool,
DLC,
Mod
Mod,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
@ -13,14 +13,14 @@ pub enum DownloadType {
pub struct DownloadableMetadata {
pub id: String,
pub version: Option<String>,
pub download_type: DownloadType
pub download_type: DownloadType,
}
impl DownloadableMetadata {
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
Self {
id,
version,
download_type
download_type,
}
}
}
}

View file

@ -1,7 +0,0 @@
use std::sync::Arc;
use super::{download_manager_builder::DownloadAgent, downloadable_metadata::DownloadableMetadata};
pub fn generate_downloadable(meta: DownloadableMetadata) -> DownloadAgent {
todo!()
}

View file

@ -0,0 +1,27 @@
use std::{fmt::Display, io, sync::mpsc::SendError};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum InternalError<T> {
IOError(io::Error),
SignalError(SendError<T>),
}
impl<T> Display for InternalError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InternalError::IOError(error) => write!(f, "{}", error),
InternalError::SignalError(send_error) => write!(f, "{}", send_error),
}
}
}
impl<T> From<SendError<T>> for InternalError<T> {
fn from(value: SendError<T>) -> Self {
InternalError::SignalError(value)
}
}
impl<T> From<io::Error> for InternalError<T> {
fn from(value: io::Error) -> Self {
InternalError::IOError(value)
}
}

View file

@ -1,9 +1,10 @@
pub mod commands;
pub mod download_manager;
pub mod download_manager_builder;
pub mod progress_object;
pub mod queue;
pub mod download_thread_control_flag;
pub mod downloadable;
pub mod application_download_error;
pub mod downloadable_metadata;
pub mod generate_downloadable;
pub mod internal_error;
pub mod progress_object;
pub mod queue;
pub mod rolling_progress_updates;

View file

@ -2,14 +2,17 @@ use std::{
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::Sender,
Arc, Mutex, RwLock,
Arc, Mutex,
},
time::Instant,
time::{Duration, Instant},
};
use log::info;
use atomic_instant_full::AtomicInstant;
use throttle_my_fn::throttle;
use super::download_manager::DownloadManagerSignal;
use super::{
download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow,
};
#[derive(Clone)]
pub struct ProgressObject {
@ -17,11 +20,10 @@ pub struct ProgressObject {
progress_instances: Arc<Mutex<Vec<Arc<AtomicUsize>>>>,
start: Arc<Mutex<Instant>>,
sender: Sender<DownloadManagerSignal>,
points_towards_update: Arc<AtomicUsize>,
points_to_push_update: Arc<AtomicUsize>,
last_update: Arc<RwLock<Instant>>,
amount_last_update: Arc<AtomicUsize>,
//last_update: Arc<RwLock<Instant>>,
last_update_time: Arc<AtomicInstant>,
bytes_last_update: Arc<AtomicUsize>,
rolling: RollingProgressWindow<250>,
}
pub struct ProgressHandle {
@ -42,72 +44,32 @@ impl ProgressHandle {
pub fn add(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
self.progress_object.check_push_update(amount);
calculate_update(&self.progress_object);
}
pub fn skip(&self, amount: usize) {
self.progress
.fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
// Offset the bytes at last offset by this amount
self.progress_object
.bytes_last_update
.fetch_add(amount, Ordering::Relaxed);
// Dont' fire update
}
}
static PROGRESS_UPDATES: usize = 100;
impl ProgressObject {
pub fn new(max: usize, length: usize, sender: Sender<DownloadManagerSignal>) -> Self {
let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect());
// TODO: consolidate this calculation with the set_max function below
let points_to_push_update = max / PROGRESS_UPDATES;
Self {
max: Arc::new(Mutex::new(max)),
progress_instances: Arc::new(arr),
start: Arc::new(Mutex::new(Instant::now())),
sender,
points_towards_update: Arc::new(AtomicUsize::new(0)),
points_to_push_update: Arc::new(AtomicUsize::new(points_to_push_update)),
last_update: Arc::new(RwLock::new(Instant::now())),
amount_last_update: Arc::new(AtomicUsize::new(0)),
}
}
pub fn check_push_update(&self, amount_added: usize) {
let current_amount = self
.points_towards_update
.fetch_add(amount_added, Ordering::Relaxed);
let to_update = self.points_to_push_update.fetch_add(0, Ordering::Relaxed);
if current_amount >= to_update {
self.points_towards_update
.fetch_sub(to_update, Ordering::Relaxed);
self.sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}
let last_update = self.last_update.read().unwrap();
let last_update_difference = Instant::now().duration_since(*last_update).as_millis();
if last_update_difference > 1000 {
// push update
drop(last_update);
let mut last_update = self.last_update.write().unwrap();
*last_update = Instant::now();
drop(last_update);
let current_amount = self.sum();
let max = self.get_max();
let amount_at_last_update = self.amount_last_update.fetch_add(0, Ordering::Relaxed);
self.amount_last_update
.store(current_amount, Ordering::Relaxed);
let amount_since_last_update = current_amount - amount_at_last_update;
let kilobytes_per_second = amount_since_last_update / (last_update_difference as usize).max(1);
let remaining = max - current_amount; // bytes
let time_remaining = (remaining / 1000) / kilobytes_per_second.max(1);
self.sender
.send(DownloadManagerSignal::UpdateUIStats(
kilobytes_per_second,
time_remaining,
))
.unwrap();
last_update_time: Arc::new(AtomicInstant::now()),
bytes_last_update: Arc::new(AtomicUsize::new(0)),
rolling: RollingProgressWindow::new(),
}
}
@ -127,9 +89,6 @@ impl ProgressObject {
}
pub fn set_max(&self, new_max: usize) {
*self.max.lock().unwrap() = new_max;
self.points_to_push_update
.store(new_max / PROGRESS_UPDATES, Ordering::Relaxed);
info!("points to push update: {}", new_max / PROGRESS_UPDATES);
}
pub fn set_size(&self, length: usize) {
*self.progress_instances.lock().unwrap() =
@ -141,4 +100,56 @@ impl ProgressObject {
pub fn get(&self, index: usize) -> Arc<AtomicUsize> {
self.progress_instances.lock().unwrap()[index].clone()
}
fn update_window(&self, kilobytes_per_second: usize) {
self.rolling.update(kilobytes_per_second);
}
}
#[throttle(1, Duration::from_millis(20))]
pub fn calculate_update(progress: &ProgressObject) {
let last_update_time = progress
.last_update_time
.swap(Instant::now(), Ordering::SeqCst);
let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis();
let current_bytes_downloaded = progress.sum();
let max = progress.get_max();
let bytes_at_last_update = progress
.bytes_last_update
.swap(current_bytes_downloaded, Ordering::Relaxed);
let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update;
let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1);
let bytes_remaining = max - current_bytes_downloaded; // bytes
progress.update_window(kilobytes_per_second);
push_update(progress, bytes_remaining);
}
#[throttle(1, Duration::from_millis(500))]
pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) {
let average_speed = progress.rolling.get_average();
let time_remaining = (bytes_remaining / 1000) / average_speed.max(1);
update_ui(progress, average_speed, time_remaining);
update_queue(progress);
}
fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) {
progress_object
.sender
.send(DownloadManagerSignal::UpdateUIStats(
kilobytes_per_second,
time_remaining,
))
.unwrap();
}
fn update_queue(progress: &ProgressObject) {
progress
.sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}

View file

@ -11,6 +11,12 @@ pub struct Queue {
}
#[allow(dead_code)]
impl Default for Queue {
fn default() -> Self {
Self::new()
}
}
impl Queue {
pub fn new() -> Self {
Self {
@ -26,7 +32,7 @@ impl Queue {
pub fn pop_front(&self) -> Option<DownloadableMetadata> {
self.edit().pop_front()
}
pub fn empty(&self) -> bool {
pub fn is_empty(&self) -> bool {
self.inner.lock().unwrap().len() == 0
}
pub fn exists(&self, meta: DownloadableMetadata) -> bool {
@ -44,15 +50,9 @@ impl Queue {
pub fn append(&self, interface: DownloadableMetadata) {
self.edit().push_back(interface);
}
pub fn pop_front_if_equal(
&self,
meta: &DownloadableMetadata,
) -> Option<DownloadableMetadata> {
pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option<DownloadableMetadata> {
let mut queue = self.edit();
let front = match queue.front() {
Some(front) => front,
None => return None,
};
let front = queue.front()?;
if front == meta {
return queue.pop_front();
}
@ -61,7 +61,11 @@ impl Queue {
pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option<usize> {
self.read().iter().position(|data| data == meta)
}
pub fn move_to_index_by_meta(&self, meta: &DownloadableMetadata, new_index: usize) -> Result<(), ()> {
pub fn move_to_index_by_meta(
&self,
meta: &DownloadableMetadata,
new_index: usize,
) -> Result<(), ()> {
let index = match self.get_by_meta(meta) {
Some(index) => index,
None => return Err(()),

View file

@ -0,0 +1,33 @@
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
#[derive(Clone)]
pub struct RollingProgressWindow<const S: usize> {
window: Arc<[AtomicUsize; S]>,
current: Arc<AtomicUsize>,
}
impl<const S: usize> RollingProgressWindow<S> {
pub fn new() -> Self {
Self {
window: Arc::new([(); S].map(|_| AtomicUsize::new(0))),
current: Arc::new(AtomicUsize::new(0)),
}
}
pub fn update(&self, kilobytes_per_second: usize) {
let index = self.current.fetch_add(1, Ordering::SeqCst);
let current = &self.window[index % S];
current.store(kilobytes_per_second, Ordering::SeqCst);
}
pub fn get_average(&self) -> usize {
let current = self.current.load(Ordering::SeqCst);
self.window
.iter()
.enumerate()
.filter(|(i, _)| i < &current)
.map(|(_, x)| x.load(Ordering::Relaxed))
.sum::<usize>()
/ S
}
}

View file

@ -1,9 +1,14 @@
use std::{fmt::{Display, Formatter}, io};
use std::{
fmt::{Display, Formatter},
io,
};
use crate::remote::RemoteAccessError;
use serde_with::SerializeDisplay;
use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
// TODO: Rename / separate from downloads
#[derive(Debug, Clone)]
#[derive(Debug, Clone, SerializeDisplay)]
pub enum ApplicationDownloadError {
Communication(RemoteAccessError),
Checksum,
@ -17,25 +22,11 @@ impl Display for ApplicationDownloadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ApplicationDownloadError::Communication(error) => write!(f, "{}", error),
ApplicationDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", error),
ApplicationDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
ApplicationDownloadError::Checksum => write!(f, "Checksum failed to validate for download"),
ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", error),
ApplicationDownloadError::Lock => write!(f, "failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
ApplicationDownloadError::Checksum => write!(f, "checksum failed to validate for download"),
ApplicationDownloadError::IoError(error) => write!(f, "{}", error),
ApplicationDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"),
ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
}
}
}
#[derive(Debug, Clone)]
pub enum SetupError {
Context,
}
impl Display for SetupError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SetupError::Context => write!(f, "Failed to generate contexts for download"),
}
}
}

View file

@ -0,0 +1,10 @@
use serde::Deserialize;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DropServerError {
pub status_code: usize,
pub status_message: String,
pub message: String,
pub url: String,
}

View file

@ -0,0 +1,19 @@
use std::fmt::Display;
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum LibraryError {
MetaNotFound(String),
}
impl Display for LibraryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LibraryError::MetaNotFound(id) => write!(
f,
"Could not locate any installed version of game ID {} in the database",
id
),
}
}
}

View file

@ -0,0 +1,6 @@
pub mod application_download_error;
pub mod drop_server_error;
pub mod library_error;
pub mod process_error;
pub mod remote_access_error;
pub mod setup_error;

View file

@ -0,0 +1,31 @@
use std::{fmt::Display, io::Error};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum ProcessError {
SetupRequired,
NotInstalled,
AlreadyRunning,
NotDownloaded,
InvalidID,
InvalidVersion,
IOError(Error),
InvalidPlatform,
}
impl Display for ProcessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
ProcessError::SetupRequired => "Game not set up",
ProcessError::NotInstalled => "Game not installed",
ProcessError::AlreadyRunning => "Game already running",
ProcessError::NotDownloaded => "Game not downloaded",
ProcessError::InvalidID => "Invalid Game ID",
ProcessError::InvalidVersion => "Invalid Game version",
ProcessError::IOError(error) => &error.to_string(),
ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
};
write!(f, "{}", s)
}
}

View file

@ -0,0 +1,69 @@
use std::{
error::Error,
fmt::{Display, Formatter},
sync::Arc,
};
use http::StatusCode;
use serde_with::SerializeDisplay;
use url::ParseError;
use super::drop_server_error::DropServerError;
#[derive(Debug, Clone, SerializeDisplay)]
pub enum RemoteAccessError {
FetchError(Arc<reqwest::Error>),
ParsingError(ParseError),
InvalidEndpoint,
HandshakeFailed(String),
GameNotFound,
InvalidResponse(DropServerError),
InvalidRedirect,
ManifestDownloadFailed(StatusCode, String),
OutOfSync,
Generic(String),
}
impl Display for RemoteAccessError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RemoteAccessError::FetchError(error) => write!(
f,
"{}: {}",
error,
error
.source()
.map(|e| e.to_string())
.or_else(|| Some("Unknown error".to_string()))
.unwrap()
),
RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{}", parse_error)
}
RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message),
RemoteAccessError::GameNotFound => write!(f, "could not find game on server"),
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message),
RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"),
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
f,
"failed to download game manifest: {} {}",
status, response
),
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
RemoteAccessError::Generic(message) => write!(f, "{}", message),
}
}
}
impl From<reqwest::Error> for RemoteAccessError {
fn from(err: reqwest::Error) -> Self {
RemoteAccessError::FetchError(Arc::new(err))
}
}
impl From<ParseError> for RemoteAccessError {
fn from(err: ParseError) -> Self {
RemoteAccessError::ParsingError(err)
}
}
impl std::error::Error for RemoteAccessError {}

View file

@ -0,0 +1,14 @@
use std::fmt::{Display, Formatter};
#[derive(Debug, Clone)]
pub enum SetupError {
Context,
}
impl Display for SetupError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SetupError::Context => write!(f, "failed to generate contexts for download"),
}
}
}

View file

@ -0,0 +1,53 @@
use std::sync::Mutex;
use tauri::AppHandle;
use crate::{
database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{get_current_meta, uninstall_game_logic}, AppState
};
use super::{
library::{
fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct,
Game,
},
state::{GameStatusManager, GameStatusWithTransient},
};
#[tauri::command]
pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
fetch_library_logic(app)
}
#[tauri::command]
pub fn fetch_game(
game_id: String,
app: tauri::AppHandle,
) -> Result<FetchGameStruct, RemoteAccessError> {
fetch_game_logic(game_id, app)
}
#[tauri::command]
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
GameStatusManager::fetch_state(&id)
}
#[tauri::command]
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
let meta = match get_current_meta(&game_id) {
Some(data) => data,
None => return Err(LibraryError::MetaNotFound(game_id)),
};
println!("{:?}", meta);
uninstall_game_logic(meta, &app_handle);
Ok(())
}
#[tauri::command]
pub fn fetch_game_verion_options(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersion>, RemoteAccessError> {
fetch_game_verion_options_logic(game_id, state)
}

View file

@ -0,0 +1,32 @@
use std::sync::{Arc, Mutex};
use crate::{
download_manager::{
download_manager::DownloadManagerSignal, downloadable::Downloadable,
internal_error::InternalError,
},
AppState,
};
use super::download_agent::GameDownloadAgent;
#[tauri::command]
pub fn download_game(
game_id: String,
game_version: String,
install_dir: usize,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), InternalError<DownloadManagerSignal>> {
let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
game_id,
game_version,
install_dir,
sender,
)) as Box<dyn Downloadable + Send + Sync>);
Ok(state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent)?)
}

View file

@ -1,25 +1,30 @@
use crate::auth::generate_authorization_header;
use crate::db::{set_game_status, GameDownloadStatus, ApplicationTransientStatus, DatabaseImpls};
use crate::download_manager::application_download_error::ApplicationDownloadError;
use crate::database::db::{
borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls,
GameDownloadStatus,
};
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
use crate::download_manager::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::downloadable::Downloadable;
use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::library::{on_game_complete, push_game_update};
use crate::remote::RemoteAccessError;
use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent};
use crate::remote::requests::make_request;
use crate::DB;
use log::{debug, error, info};
use rayon::ThreadPoolBuilder;
use tauri::{AppHandle, Emitter};
use std::collections::VecDeque;
use std::fs::{create_dir_all, remove_dir_all, File};
use slice_deque::SliceDeque;
use std::fs::{create_dir_all, File};
use std::path::Path;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use std::thread::spawn;
use std::time::Instant;
use tauri::{AppHandle, Emitter};
use urlencoding::encode;
#[cfg(target_os = "linux")]
@ -33,12 +38,12 @@ pub struct GameDownloadAgent {
pub version: String,
pub control_flag: DownloadThreadControl,
contexts: Mutex<Vec<DropDownloadContext>>,
completed_contexts: Mutex<VecDeque<usize>>,
completed_contexts: Mutex<SliceDeque<usize>>,
pub manifest: Mutex<Option<DropManifest>>,
pub progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>,
pub stored_manifest: StoredManifest,
status: Mutex<DownloadStatus>
status: Mutex<DownloadStatus>,
}
impl GameDownloadAgent {
@ -51,7 +56,7 @@ impl GameDownloadAgent {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let db_lock = DB.borrow_data().unwrap();
let db_lock = borrow_db_checked();
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
drop(db_lock);
@ -67,7 +72,7 @@ impl GameDownloadAgent {
control_flag,
manifest: Mutex::new(None),
contexts: Mutex::new(Vec::new()),
completed_contexts: Mutex::new(VecDeque::new()),
completed_contexts: Mutex::new(SliceDeque::new()),
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
sender,
stored_manifest,
@ -78,10 +83,8 @@ impl GameDownloadAgent {
// Blocking
pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
self.ensure_manifest_exists()?;
info!("Ensured manifest exists");
self.ensure_contexts()?;
info!("Ensured contexts exists");
self.control_flag.set(DownloadThreadControlFlag::Go);
@ -90,16 +93,24 @@ impl GameDownloadAgent {
// Blocking
pub fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
info!("Setting up download");
self.setup_download()?;
info!("Setting progress object params");
self.set_progress_object_params();
info!("Running");
let timer = Instant::now();
push_game_update(app_handle, &self.metadata(), (None, Some(ApplicationTransientStatus::Downloading { version_name: self.version.clone() })));
let res = self.run().map_err(|_| ApplicationDownloadError::DownloadError);
push_game_update(
app_handle,
&self.metadata().id,
(
None,
Some(ApplicationTransientStatus::Downloading {
version_name: self.version.clone(),
}),
),
);
let res = self
.run()
.map_err(|_| ApplicationDownloadError::DownloadError);
info!(
debug!(
"{} took {}ms to download",
self.id,
timer.elapsed().as_millis()
@ -116,25 +127,17 @@ impl GameDownloadAgent {
}
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
let base_url = DB.fetch_base_url();
let manifest_url = base_url
.join(
format!(
"/api/v1/client/metadata/manifest?id={}&version={}",
self.id,
encode(&self.version)
)
.as_str(),
)
.unwrap();
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(manifest_url.to_string())
.header("Authorization", header)
.send()
.unwrap();
let response = make_request(
&client,
&["/api/v1/client/game/manifest"],
&[("id", &self.id), ("version", &self.version)],
|f| f.header("Authorization", header),
)
.map_err(|e| ApplicationDownloadError::Communication(e))?
.send()
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
if response.status() != 200 {
return Err(ApplicationDownloadError::Communication(
@ -145,7 +148,7 @@ impl GameDownloadAgent {
));
}
let manifest_download = response.json::<DropManifest>().unwrap();
let manifest_download: DropManifest = response.json().unwrap();
if let Ok(mut manifest) = self.manifest.lock() {
*manifest = Some(manifest_download);
@ -167,11 +170,8 @@ impl GameDownloadAgent {
let chunk_count = contexts.iter().map(|chunk| chunk.length).sum();
debug!("Setting ProgressObject max to {}", chunk_count);
self.progress.set_max(chunk_count);
debug!("Setting ProgressObject size to {}", length);
self.progress.set_size(length);
debug!("Setting ProgressObject time to now");
self.progress.set_time_now();
}
@ -192,12 +192,11 @@ impl GameDownloadAgent {
let base_path = Path::new(&self.stored_manifest.base_path);
create_dir_all(base_path).unwrap();
{
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
completed_contexts_lock.clear();
completed_contexts_lock
.extend(self.stored_manifest.get_completed_contexts());
.extend_from_slice(&self.stored_manifest.get_completed_contexts());
}
for (raw_path, chunk) in manifest {
@ -234,22 +233,22 @@ impl GameDownloadAgent {
Ok(())
}
// TODO: Change return value on Err
pub fn run(&self) -> Result<bool, ()> {
info!("downloading game: {}", self.id);
const DOWNLOAD_MAX_THREADS: usize = 1;
let max_download_threads = borrow_db_checked().settings.max_download_threads;
debug!(
"downloading game: {} with {} threads",
self.id, max_download_threads
);
let pool = ThreadPoolBuilder::new()
.num_threads(DOWNLOAD_MAX_THREADS)
.num_threads(max_download_threads)
.build()
.unwrap();
let completed_indexes = Arc::new(boxcar::Vec::new());
let completed_indexes_loop_arc = completed_indexes.clone();
let base_url = DB.fetch_base_url();
let contexts = self.contexts.lock().unwrap();
pool.scope(|scope| {
let client = &reqwest::blocking::Client::new();
@ -259,19 +258,36 @@ impl GameDownloadAgent {
let progress = self.progress.get(index);
let progress_handle = ProgressHandle::new(progress, self.progress.clone());
// If we've done this one already, skip it
if self.completed_contexts.lock().unwrap().contains(&index) {
progress_handle.add(context.length);
progress_handle.skip(context.length);
continue;
}
let sender = self.sender.clone();
let request = generate_request(&base_url, client, &context);
let request = match make_request(
&client,
&["/api/v1/client/chunk"],
&[
("id", &context.game_id),
("version", &context.version),
("name", &context.file_name),
("chunk", &context.index.to_string()),
],
|r| r.header("Authorization", generate_authorization_header()),
) {
Ok(request) => request,
Err(e) => {
sender.send(DownloadManagerSignal::Error(ApplicationDownloadError::Communication(e))).unwrap();
continue;
},
};
scope.spawn(move |_| {
match download_game_chunk(context, &self.control_flag, progress_handle, request) {
match download_game_chunk(context, &self.control_flag, progress_handle, request)
{
Ok(res) => {
if res {
completed_indexes.push(index);
@ -282,39 +298,35 @@ impl GameDownloadAgent {
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
info!("Completed context id {}", index);
});
}
});
let newly_completed = completed_indexes.to_owned();
let completed_lock_len = {
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
for (item, _) in newly_completed.iter() {
completed_contexts_lock.push_front(item);
for (_, item) in newly_completed.iter() {
completed_contexts_lock.push_front(*item);
}
completed_contexts_lock.len()
};
info!("Got newly completed");
// If we're not out of contexts, we're not done, so we don't fire completed
if completed_lock_len != contexts.len() {
info!("da for {} exited without completing", self.id.clone());
info!(
"download agent for {} exited without completing ({}/{})",
self.id.clone(),
completed_lock_len,
contexts.len(),
);
self.stored_manifest
.set_completed_contexts(&self.completed_contexts.lock().unwrap().clone().into());
info!("Setting completed contexts");
.set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
self.stored_manifest.write();
info!("Wrote completed contexts");
return Ok(false);
}
info!("Sending completed signal");
// We've completed
self.sender
.send(DownloadManagerSignal::Completed(self.metadata()))
@ -324,26 +336,6 @@ impl GameDownloadAgent {
}
}
fn generate_request(base_url: &url::Url, client: reqwest::blocking::Client, context: &DropDownloadContext) -> reqwest::blocking::RequestBuilder {
let chunk_url = base_url
.join(&format!(
"/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
// Encode the parts we don't trust
context.game_id,
encode(&context.version),
encode(&context.file_name),
context.index
))
.unwrap();
let header = generate_authorization_header();
let request = client
.get(chunk_url)
.header("Authorization", header);
request
}
impl Downloadable for GameDownloadAgent {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
*self.status.lock().unwrap() = DownloadStatus::Downloading;
@ -368,7 +360,6 @@ impl Downloadable for GameDownloadAgent {
fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
*self.status.lock().unwrap() = DownloadStatus::Queued;
return;
}
fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) {
@ -382,23 +373,35 @@ impl Downloadable for GameDownloadAgent {
set_game_status(app_handle, self.metadata(), |db_handle, meta| {
db_handle.applications.transient_statuses.remove(meta);
});
}
fn on_complete(&self, app_handle: &tauri::AppHandle) {
on_game_complete(&self.metadata(), self.stored_manifest.base_path.to_string_lossy().to_string(), app_handle).unwrap();
on_game_complete(
&self.metadata(),
self.stored_manifest.base_path.to_string_lossy().to_string(),
app_handle,
)
.unwrap();
}
fn on_incomplete(&self, _app_handle: &tauri::AppHandle) {
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
let meta = self.metadata();
*self.status.lock().unwrap() = DownloadStatus::Queued;
return;
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(GameDownloadStatus::Remote {}), None),
},
)
.unwrap();
}
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {
return;
}
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
fn status(&self) -> DownloadStatus {
self.status.lock().unwrap().clone()
}
}
}

View file

@ -1,68 +0,0 @@
use std::sync::{Arc, Mutex};
use crate::{download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, AppState};
use super::download_agent::GameDownloadAgent;
#[tauri::command]
pub fn download_game(
game_id: String,
game_version: String,
install_dir: usize,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), String> {
let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(
Box::new(GameDownloadAgent::new(game_id, game_version, install_dir, sender)) as Box<dyn Downloadable + Send + Sync>
);
state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent)
.map_err(|_| "An error occurred while communicating with the download manager.".to_string())
}
#[tauri::command]
pub fn pause_game_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.pause_downloads()
}
#[tauri::command]
pub fn resume_game_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.resume_downloads()
}
#[tauri::command]
pub fn move_game_in_queue(
state: tauri::State<'_, Mutex<AppState>>,
old_index: usize,
new_index: usize,
) {
state
.lock()
.unwrap()
.download_manager
.rearrange(old_index, new_index)
}
#[tauri::command]
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
state.lock().unwrap().download_manager.cancel(meta)
}
/*
#[tauri::command]
pub fn get_current_write_speed(state: tauri::State<'_, Mutex<AppState>>) {}
*/
/*
fn use_download_agent(
state: tauri::State<'_, Mutex<AppState>>,
game_id: String,
) -> Result<Arc<GameDownloadAgent>, String> {
let lock = state.lock().unwrap();
let download_agent = lock.download_manager.get(&game_id).ok_or("Invalid game ID")?;
Ok(download_agent.clone()) // Clones the Arc, not the underlying data structure
}
*/

View file

@ -1,26 +1,23 @@
use crate::auth::generate_authorization_header;
use crate::db::DatabaseImpls;
use crate::download_manager::application_download_error::ApplicationDownloadError;
use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
use crate::download_manager::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::progress_object::ProgressHandle;
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::DropDownloadContext;
use crate::remote::{DropServerError, RemoteAccessError};
use crate::DB;
use log::{error, info, warn};
use log::warn;
use md5::{Context, Digest};
use reqwest::blocking::{Client, Request, RequestBuilder, Response};
use reqwest::blocking::{RequestBuilder, Response};
use std::fs::{set_permissions, Permissions};
use std::io::Read;
use std::io::{ErrorKind, Read};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::sync::Arc;
use std::{
fs::{File, OpenOptions},
io::{self, BufWriter, Seek, SeekFrom, Write},
path::PathBuf,
};
use urlencoding::encode;
pub struct DropWriter<W: Write> {
hasher: Context,
@ -42,19 +39,17 @@ impl DropWriter<File> {
// Write automatically pushes to file and hasher
impl Write for DropWriter<File> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
/*
self.hasher.write_all(buf).map_err(|e| {
io::Error::new(
ErrorKind::Other,
format!("Unable to write to hasher: {}", e),
)
})?;
*/
self.destination.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
// self.hasher.flush()?;
self.hasher.flush()?;
self.destination.flush()
}
}
@ -124,7 +119,7 @@ pub fn download_game_chunk(
ctx: &DropDownloadContext,
control_flag: &DownloadThreadControl,
progress: ProgressHandle,
request: RequestBuilder
request: RequestBuilder,
) -> Result<bool, ApplicationDownloadError> {
// If we're paused
if control_flag.get() == DownloadThreadControlFlag::Stop {
@ -137,9 +132,9 @@ pub fn download_game_chunk(
.map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
if response.status() != 200 {
warn!("{}", response.text().unwrap());
let err = response.json().unwrap();
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidCodeError(400),
RemoteAccessError::InvalidResponse(err),
));
}
@ -153,9 +148,9 @@ pub fn download_game_chunk(
let content_length = response.content_length();
if content_length.is_none() {
error!("Recieved 0 length content from server");
warn!("recieved 0 length content from server");
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(response.json::<DropServerError>().unwrap()),
RemoteAccessError::InvalidResponse(response.json().unwrap()),
));
}
@ -181,16 +176,14 @@ pub fn download_game_chunk(
set_permissions(ctx.path.clone(), permissions).unwrap();
}
/*
let checksum = pipeline
.finish()
.map_err(|e| GameDownloadError::IoError(e))?;
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
let res = hex::encode(checksum.0);
if res != ctx.checksum {
return Err(GameDownloadError::Checksum);
return Err(ApplicationDownloadError::Checksum);
}
*/
Ok(true)
}

View file

@ -1,5 +1,5 @@
pub mod commands;
pub mod download_agent;
pub mod download_commands;
mod download_logic;
mod manifest;
mod stored_manifest;
mod stored_manifest;

View file

@ -5,7 +5,7 @@ use std::{
sync::Mutex,
};
use log::error;
use log::{error, warn};
use serde::{Deserialize, Serialize};
use serde_binary::binary_stream::Endian;
@ -43,12 +43,10 @@ impl StoredManifest {
}
};
match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
Ok(manifest) => manifest,
Err(e) => {
error!("{}", e);
warn!("{}", e);
StoredManifest::new(game_id, game_version, base_path)
}
}
@ -72,8 +70,8 @@ impl StoredManifest {
Err(e) => error!("{}", e),
};
}
pub fn set_completed_contexts(&self, completed_contexts: &Vec<usize>) {
*self.completed_contexts.lock().unwrap() = completed_contexts.clone();
pub fn set_completed_contexts(&self, completed_contexts: &[usize]) {
*self.completed_contexts.lock().unwrap() = completed_contexts.to_owned();
}
pub fn get_completed_contexts(&self) -> Vec<usize> {
self.completed_contexts.lock().unwrap().clone()

View file

@ -2,20 +2,20 @@ use std::fs::remove_dir_all;
use std::sync::Mutex;
use std::thread::spawn;
use log::{error, info, warn};
use log::{debug, error, warn};
use serde::{Deserialize, Serialize};
use tauri::Emitter;
use tauri::{AppHandle, Manager};
use urlencoding::encode;
use crate::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
use crate::db::GameVersion;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion};
use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus};
use crate::download_manager::download_manager::DownloadStatus;
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
use crate::process::process_manager::Platform;
use crate::remote::RemoteAccessError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::{auth::generate_authorization_header, AppState, DB};
use crate::remote::auth::generate_authorization_header;
use crate::remote::requests::make_request;
use crate::AppState;
#[derive(serde::Serialize)]
pub struct FetchGameStruct {
@ -40,7 +40,10 @@ pub struct Game {
#[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent {
pub game_id: String,
pub status: (Option<GameDownloadStatus>, Option<ApplicationTransientStatus>),
pub status: (
Option<GameDownloadStatus>,
Option<ApplicationTransientStatus>,
),
}
#[derive(Serialize, Clone)]
@ -48,6 +51,8 @@ pub struct QueueUpdateEventQueueData {
pub meta: DownloadableMetadata,
pub status: DownloadStatus,
pub progress: f64,
pub current: usize,
pub max: usize,
}
#[derive(serde::Serialize, Clone)]
@ -61,42 +66,27 @@ pub struct StatsUpdateEvent {
pub time: usize,
}
// Game version with some fields missing and size information
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameVersionOption {
version_index: usize,
version_name: String,
platform: Platform,
setup_command: String,
launch_command: String,
delta: bool,
umu_id_override: Option<String>,
// total_size: usize,
}
fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let library_url = base_url.join("/api/v1/client/user/library")?;
pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(library_url.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
f.header("Authorization", header)
})?
.send()?;
if response.status() != 200 {
return Err(response.status().as_u16().into());
let err = response.json().unwrap();
warn!("{:?}", err);
return Err(RemoteAccessError::InvalidResponse(err));
}
let games: Vec<Game> = response.json::<Vec<Game>>()?;
let games: Vec<Game> = response.json()?;
let state = app.state::<Mutex<AppState>>();
let mut handle = state.lock().unwrap();
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
for game in games.iter() {
handle.games.insert(game.id.clone(), game.clone());
@ -113,12 +103,7 @@ fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessError> {
Ok(games)
}
#[tauri::command]
pub fn fetch_library(app: AppHandle) -> Result<Vec<Game>, String> {
fetch_library_logic(app).map_err(|e| e.to_string())
}
fn fetch_game_logic(
pub fn fetch_game_logic(
id: String,
app: tauri::AppHandle,
) -> Result<FetchGameStruct, RemoteAccessError> {
@ -136,31 +121,25 @@ fn fetch_game_logic(
return Ok(data);
}
let base_url = DB.fetch_base_url();
let endpoint = base_url.join(&format!("/api/v1/game/{}", id))?;
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(&client, &["/api/v1/game/", &id], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
.send()?;
if response.status() == 404 {
return Err(RemoteAccessError::GameNotFound);
}
if response.status() != 200 {
return Err(RemoteAccessError::InvalidCodeError(
response.status().into(),
));
let err = response.json().unwrap();
warn!("{:?}", err);
return Err(RemoteAccessError::InvalidResponse(err));
}
let game = response.json::<Game>()?;
let game: Game = response.json()?;
state_handle.games.insert(id.clone(), game.clone());
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
@ -179,95 +158,62 @@ fn fetch_game_logic(
Ok(data)
}
#[tauri::command]
pub fn fetch_game(game_id: String, app: tauri::AppHandle) -> Result<FetchGameStruct, String> {
let result = fetch_game_logic(game_id, app);
if result.is_err() {
return Err(result.err().unwrap().to_string());
}
Ok(result.unwrap())
}
#[tauri::command]
pub fn fetch_game_status(id: String) -> Result<GameStatusWithTransient, String> {
let status = GameStatusManager::fetch_state(&id);
Ok(status)
}
fn fetch_game_verion_options_logic<'a>(
pub fn fetch_game_verion_options_logic(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersionOption>, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let endpoint =
base_url.join(format!("/api/v1/client/metadata/versions?id={}", game_id).as_str())?;
let header = generate_authorization_header();
) -> Result<Vec<GameVersion>, RemoteAccessError> {
let client = reqwest::blocking::Client::new();
let response = client
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(
&client,
&["/api/v1/client/game/versions"],
&[("id", &game_id)],
|r| r.header("Authorization", generate_authorization_header()),
)?
.send()?;
if response.status() != 200 {
return Err(RemoteAccessError::InvalidCodeError(
response.status().into(),
));
let err = response.json().unwrap();
warn!("{:?}", err);
return Err(RemoteAccessError::InvalidResponse(err));
}
let data = response.json::<Vec<GameVersionOption>>()?;
let data: Vec<GameVersion> = response.json()?;
let state_lock = state.lock().unwrap();
let process_manager_lock = state_lock.process_manager.lock().unwrap();
let data = data
let data: Vec<GameVersion> = data
.into_iter()
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
.collect::<Vec<GameVersionOption>>();
.collect();
drop(process_manager_lock);
drop(state_lock);
Ok(data)
}
#[tauri::command]
pub fn uninstall_game(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
app_handle: AppHandle
) -> Result<(), String> {
let meta = get_current_meta(&game_id)?;
println!("{:?}", meta);
uninstall_game_logic(meta, &app_handle);
Ok(())
}
fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
println!("Triggered uninstall for agent");
let mut db_handle = DB.borrow_data_mut().unwrap();
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
println!("triggered uninstall for agent");
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.transient_statuses
.entry(meta.clone())
.and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
push_game_update(
app_handle,
&meta,
&meta.id,
(None, Some(ApplicationTransientStatus::Uninstalling {})),
);
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
if previous_state.is_none() {
info!("uninstall job doesn't have previous state, failing silently");
warn!("uninstall job doesn't have previous state, failing silently");
return;
}
let previous_state = previous_state.unwrap();
if let Some((version_name, install_dir)) = match previous_state {
if let Some((_, install_dir)) = match previous_state {
GameDownloadStatus::Installed {
version_name,
install_dir,
@ -291,7 +237,7 @@ fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
error!("{}", e);
}
Ok(_) => {
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
db_handle.applications.transient_statuses.remove(&meta);
db_handle
.applications
@ -299,29 +245,26 @@ fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
.entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {});
drop(db_handle);
DB.save().unwrap();
save_db();
info!("uninstalled game id {}", &meta.id);
debug!("uninstalled game id {}", &meta.id);
push_game_update(&app_handle, &meta, (Some(GameDownloadStatus::Remote {}), None));
push_game_update(
&app_handle,
&meta.id,
(Some(GameDownloadStatus::Remote {}), None),
);
}
});
}
}
pub fn get_current_meta(game_id: &String) -> Result<DownloadableMetadata, String> {
match DB.borrow_data().unwrap().applications.installed_game_version.get(game_id) {
Some(meta) => Ok(meta.clone()),
None => Err(String::from("Could not find installed version")),
}
}
#[tauri::command]
pub fn fetch_game_verion_options<'a>(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersionOption>, String> {
fetch_game_verion_options_logic(game_id, state).map_err(|e| e.to_string())
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
borrow_db_checked()
.applications
.installed_game_version
.get(game_id)
.cloned()
}
pub fn on_game_complete(
@ -330,28 +273,27 @@ pub fn on_game_complete(
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
let base_url = DB.fetch_base_url();
if meta.version.is_none() { return Err(RemoteAccessError::GameNotFound) }
if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound);
}
let endpoint = base_url.join(
format!(
"/api/v1/client/metadata/version?id={}&version={}",
meta.id,
encode(meta.version.as_ref().unwrap())
)
.as_str(),
)?;
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(
&client,
&["/api/v1/client/metadata/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
|f| f.header("Authorization", header),
)?
.send()?;
let data = response.json::<GameVersion>()?;
let data: GameVersion = response.json()?;
let mut handle = DB.borrow_data_mut().unwrap();
let mut handle = borrow_db_mut_checked();
handle
.applications
.game_versions
@ -364,7 +306,7 @@ pub fn on_game_complete(
.insert(meta.id.clone(), meta.clone());
drop(handle);
DB.save().unwrap();
save_db();
let status = if data.setup_command.is_empty() {
GameDownloadStatus::Installed {
@ -378,13 +320,13 @@ pub fn on_game_complete(
}
};
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(db_handle);
DB.save().unwrap();
save_db();
app_handle
.emit(
&format!("update_game/{}", meta.id),
@ -398,14 +340,14 @@ pub fn on_game_complete(
Ok(())
}
pub fn push_game_update(app_handle: &AppHandle, meta: &DownloadableMetadata, status: GameStatusWithTransient) {
pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameStatusWithTransient) {
app_handle
.emit(
&format!("update_game/{}", meta.id),
&format!("update_game/{}", game_id),
GameUpdateEvent {
game_id: meta.id.clone(),
game_id: game_id.clone(),
status,
},
)
.unwrap();
}
}

View file

@ -1,3 +1,4 @@
pub mod commands;
pub mod downloads;
pub mod library;
pub mod state;
pub mod state;

View file

@ -1,19 +1,16 @@
use crate::{
db::{ApplicationTransientStatus, GameDownloadStatus}, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, fetch_state, DB
};
use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus};
pub type GameStatusWithTransient = (Option<GameDownloadStatus>, Option<ApplicationTransientStatus>);
pub type GameStatusWithTransient = (
Option<GameDownloadStatus>,
Option<ApplicationTransientStatus>,
);
pub struct GameStatusManager {}
impl GameStatusManager {
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
let db_lock = DB.borrow_data().unwrap();
let db_lock = borrow_db_checked();
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
Some(meta) => db_lock
.applications
.transient_statuses
.get(meta)
.cloned(),
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
None => None,
};
let offline_state = db_lock.applications.game_statuses.get(game_id).cloned();
@ -29,4 +26,4 @@ impl GameStatusManager {
(None, None)
}
}
}

View file

@ -1,50 +1,54 @@
mod auth;
mod db;
mod database;
mod games;
mod autostart;
mod cleanup;
mod debug;
mod commands;
mod download_manager;
mod error;
mod process;
mod remote;
mod tools;
pub mod download_manager;
#[cfg(test)]
mod tests;
use crate::autostart::{get_autostart_enabled, toggle_autostart};
use crate::db::DatabaseImpls;
use auth::{
auth_initiate, generate_authorization_header, manual_recieve_handshake, recieve_handshake,
retry_connect, sign_out,
};
use crate::database::db::DatabaseImpls;
use autostart::{get_autostart_enabled, toggle_autostart};
use cleanup::{cleanup_and_exit, quit};
use db::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, GameDownloadStatus,
DATA_ROOT_DIR,
use commands::fetch_state;
use database::commands::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
fetch_system_data, update_settings,
};
use database::db::{
borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR,
};
use download_manager::commands::{
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
};
use debug::fetch_system_data;
use download_manager::download_manager::DownloadManager;
use download_manager::download_manager_builder::DownloadManagerBuilder;
use games::downloads::download_commands::{cancel_game, download_game, move_game_in_queue, pause_game_downloads, resume_game_downloads};
use games::commands::{
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game,
};
use games::downloads::commands::download_game;
use games::library::Game;
use http::Response;
use http::{header::*, response::Builder as ResponseBuilder};
use games::library::{
fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, Game
};
use log::{debug, info, warn, LevelFilter};
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
use log4rs::Config;
use process::compat::CompatibilityManager;
use process::process_commands::{kill_game, launch_game};
use process::commands::{kill_game, launch_game};
use process::process_manager::ProcessManager;
use remote::{gen_drop_url, use_remote};
use remote::auth::{self, generate_authorization_header, recieve_handshake};
use remote::commands::{
auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote,
};
use remote::requests::make_request;
use serde::{Deserialize, Serialize};
use tauri_plugin_dialog::DialogExt;
use std::env;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::{
collections::HashMap,
@ -52,8 +56,9 @@ use std::{
};
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::TrayIconBuilder;
use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent};
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_dialog::DialogExt;
#[derive(Clone, Copy, Serialize)]
pub enum AppStatus {
@ -86,29 +91,25 @@ pub struct AppState<'a> {
download_manager: Arc<DownloadManager>,
#[serde(skip_serializing)]
process_manager: Arc<Mutex<ProcessManager<'a>>>,
#[serde(skip_serializing)]
compat_manager: Arc<Mutex<CompatibilityManager>>,
}
#[tauri::command]
fn fetch_state(state: tauri::State<'_, Mutex<AppState<'_>>>) -> Result<String, String> {
let guard = state.lock().unwrap();
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
drop(guard);
Ok(cloned_state)
}
fn setup(handle: AppHandle) -> AppState<'static> {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
.encoder(Box::new(PatternEncoder::new(
"{d} | {l} | {f}:{L} - {m}{n}",
)))
.append(false)
.build(DATA_ROOT_DIR.lock().unwrap().join("./drop.log"))
.unwrap();
let console = ConsoleAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
.encoder(Box::new(PatternEncoder::new(
"{d} | {l} | {f}:{L} - {m}{n}",
)))
.build();
let log_level = env::var("RUST_LOG").unwrap_or(String::from("Info"));
let config = Config::builder()
.appenders(vec![
Appender::builder().build("logfile", Box::new(logfile)),
@ -117,7 +118,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
.build(
Root::builder()
.appenders(vec!["logfile", "console"])
.build(LevelFilter::Info),
.build(LevelFilter::from_str(&log_level).expect("Invalid log level")),
)
.unwrap();
@ -126,9 +127,8 @@ fn setup(handle: AppHandle) -> AppState<'static> {
let games = HashMap::new();
let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
let compat_manager = Arc::new(Mutex::new(CompatibilityManager::new()));
debug!("Checking if database is set up");
debug!("checking if database is set up");
let is_set_up = DB.database_is_set_up();
if !is_set_up {
return AppState {
@ -137,22 +137,22 @@ fn setup(handle: AppHandle) -> AppState<'static> {
games,
download_manager,
process_manager,
compat_manager,
};
}
debug!("Database is set up");
debug!("database is set up");
let (app_status, user) = auth::setup().unwrap();
// TODO: Account for possible failure
let (app_status, user) = auth::setup();
let db_handle = DB.borrow_data().unwrap();
let db_handle = borrow_db_checked();
let mut missing_games = Vec::new();
let statuses = db_handle.applications.game_statuses.clone();
drop(db_handle);
for (game_id, status) in statuses.into_iter() {
match status {
db::GameDownloadStatus::Remote {} => {}
db::GameDownloadStatus::SetupRequired {
database::db::GameDownloadStatus::Remote {} => {}
database::db::GameDownloadStatus::SetupRequired {
version_name: _,
install_dir,
} => {
@ -161,7 +161,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
missing_games.push(game_id);
}
}
db::GameDownloadStatus::Installed {
database::db::GameDownloadStatus::Installed {
version_name: _,
install_dir,
} => {
@ -175,7 +175,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
info!("detected games missing: {:?}", missing_games);
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
for game_id in missing_games {
db_handle
.applications
@ -186,12 +186,11 @@ fn setup(handle: AppHandle) -> AppState<'static> {
drop(db_handle);
info!("finished setup!");
debug!("finished setup!");
// Sync autostart state
if let Err(e) = autostart::sync_autostart_on_startup(&handle) {
warn!("Failed to sync autostart state: {}", e);
warn!("failed to sync autostart state: {}", e);
}
AppState {
@ -200,7 +199,6 @@ fn setup(handle: AppHandle) -> AppState<'static> {
games,
download_manager,
process_manager,
compat_manager,
}
}
@ -227,6 +225,9 @@ pub fn run() {
fetch_state,
quit,
fetch_system_data,
// User utils
update_settings,
fetch_settings,
// Auth
auth_initiate,
retry_connect,
@ -245,9 +246,9 @@ pub fn run() {
fetch_game_verion_options,
// Downloads
download_game,
move_game_in_queue,
pause_game_downloads,
resume_game_downloads,
move_download_in_queue,
pause_downloads,
resume_downloads,
cancel_game,
uninstall_game,
// Processes
@ -265,14 +266,14 @@ pub fn run() {
.setup(|app| {
let handle = app.handle().clone();
let state = setup(handle);
info!("initialized drop client");
debug!("initialized drop client");
app.manage(Mutex::new(state));
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?;
info!("registered all pre-defined deep links");
debug!("registered all pre-defined deep links");
}
let handle = app.handle().clone();
@ -292,7 +293,7 @@ pub fn run() {
.unwrap();
app.deep_link().on_open_url(move |event| {
info!("handling drop:// url");
debug!("handling drop:// url");
let binding = event.urls();
let url = binding.first().unwrap();
if url.host_str().unwrap() == "handshake" {
@ -322,48 +323,50 @@ pub fn run() {
app.webview_windows().get("main").unwrap().show().unwrap();
}
"quit" => {
cleanup_and_exit(app);
cleanup_and_exit(app, &app.state());
}
_ => {
println!("Menu event not handled: {:?}", event.id);
println!("menu event not handled: {:?}", event.id);
}
})
.build(app)
.expect("error while setting up tray menu");
{
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
if let Some(original) = db_handle.prev_database.take() {
warn!("Database corrupted. Original file at {}", original.canonicalize().unwrap().to_string_lossy().to_string());
warn!(
"Database corrupted. Original file at {}",
original
.canonicalize()
.unwrap()
.to_string_lossy()
.to_string()
);
app.dialog()
.message("Database corrupted. A copy has been saved at: ".to_string() + original.to_str().unwrap())
.message(
"Database corrupted. A copy has been saved at: ".to_string()
+ original.to_str().unwrap(),
)
.title("Database corrupted")
.show(|_| {});
}
}
Ok(())
})
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
let base_url = DB.fetch_base_url();
// Drop leading /
let object_id = &request.uri().path()[1..];
let object_url = base_url
.join("/api/v1/client/object/")
.unwrap()
.join(object_id)
.unwrap();
let header = generate_authorization_header();
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
let response = client
.get(object_url.to_string())
.header("Authorization", header)
.send();
let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
f.header("Authorization", header)
})
.unwrap()
.send();
if response.is_err() {
warn!(
"failed to fetch object with error: {}",
@ -392,7 +395,7 @@ pub fn run() {
.build(tauri::generate_context!())
.expect("error while running tauri application");
app.run(|app_handle, event| {
app.run(|_app_handle, event| {
if let RunEvent::ExitRequested { code, api, .. } = event {
if code.is_none() {
api.prevent_exit();

View file

@ -0,0 +1,40 @@
use std::sync::Mutex;
use crate::{error::process_error::ProcessError, AppState};
#[tauri::command]
pub fn launch_game(
id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), ProcessError> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
//let meta = DownloadableMetadata {
// id,
// version: Some(version),
// download_type: DownloadType::Game,
//};
match process_manager_lock.launch_process(id) {
Ok(_) => {}
Err(e) => return Err(e),
};
drop(process_manager_lock);
drop(state_lock);
Ok(())
}
#[tauri::command]
pub fn kill_game(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), ProcessError> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
process_manager_lock
.kill_game(game_id)
.map_err(ProcessError::IOError)
}

View file

@ -1,51 +1,13 @@
use std::{
fs::create_dir_all,
path::PathBuf,
sync::atomic::{AtomicBool, Ordering},
};
// Since this code isn't being used, we can either:
// 1. Delete the entire file if compatibility features are not planned
// 2. Or add a TODO comment if planning to implement later
use crate::db::DATA_ROOT_DIR;
pub struct CompatibilityManager {
compat_tools_path: PathBuf,
prefixes_path: PathBuf,
created_paths: AtomicBool,
}
// Option 1: Delete the file
// Delete src-tauri/src/process/compat.rs
// Option 2: Add TODO comment
/*
This gets built into both the Windows & Linux client, but
we only need it in the Linux client. Therefore, it should
do nothing but take a little bit of memory if we're on
Windows.
TODO: Compatibility layer for running Windows games on Linux
This module is currently unused but reserved for future implementation
of Windows game compatibility features on Linux.
*/
impl CompatibilityManager {
pub fn new() -> Self {
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
let compat_tools_path = root_dir_lock.join("compatibility_tools");
let prefixes_path = root_dir_lock.join("prefixes");
drop(root_dir_lock);
Self {
compat_tools_path,
prefixes_path,
created_paths: AtomicBool::new(false),
}
}
fn ensure_paths_exist(&self) -> Result<(), String> {
if self.created_paths.fetch_and(true, Ordering::Relaxed) {
return Ok(());
}
if !self.compat_tools_path.exists() {
create_dir_all(self.compat_tools_path.clone()).map_err(|e| e.to_string())?;
}
if !self.prefixes_path.exists() {
create_dir_all(self.prefixes_path.clone()).map_err(|e| e.to_string())?;
}
self.created_paths.store(true, Ordering::Relaxed);
Ok(())
}
}

View file

@ -1,3 +1,3 @@
pub mod commands;
pub mod compat;
pub mod process_commands;
pub mod process_manager;

View file

@ -1,40 +0,0 @@
use std::sync::Mutex;
use crate::{db::GameDownloadStatus, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, games::library::get_current_meta, AppState, DB};
#[tauri::command]
pub fn launch_game(
id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), String> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
let version = match DB.borrow_data().unwrap().applications.game_statuses.get(&id).cloned() {
Some(GameDownloadStatus::Installed { version_name, install_dir }) => version_name,
Some(GameDownloadStatus::SetupRequired { version_name, install_dir }) => return Err(String::from("Game setup still required")),
_ => return Err(String::from("Game not installed"))
};
let meta = DownloadableMetadata { id, version: Some(version), download_type: DownloadType::Game };
process_manager_lock.launch_process(meta)?;
drop(process_manager_lock);
drop(state_lock);
Ok(())
}
#[tauri::command]
pub fn kill_game(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), String> {
let meta = get_current_meta(&game_id)?;
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
process_manager_lock.kill_game(meta).map_err(|x| x.to_string())
}

View file

@ -1,27 +1,33 @@
use std::{
collections::HashMap,
fs::{File, OpenOptions},
io,
io::{self, Error},
path::{Path, PathBuf},
process::{Child, Command, ExitStatus},
sync::{Arc, Mutex},
thread::spawn,
};
use log::{info, warn};
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use shared_child::SharedChild;
use tauri::{AppHandle, Manager};
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{
db::{GameDownloadStatus, ApplicationTransientStatus, DATA_ROOT_DIR}, download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, games::library::push_game_update, games::state::GameStatusManager, AppState, DB
database::db::{
borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion, DATA_ROOT_DIR
},
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager},
AppState, DB,
};
pub struct ProcessManager<'a> {
current_platform: Platform,
log_output_dir: PathBuf,
processes: HashMap<DownloadableMetadata, Arc<SharedChild>>,
processes: HashMap<String, Arc<SharedChild>>,
app_handle: AppHandle,
game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
}
@ -60,13 +66,8 @@ impl ProcessManager<'_> {
}
}
// There's no easy way to distinguish between an executable name with
// spaces and it's arguments.
// I think if we just join the install_dir to whatever the user provides us, we'll be alright
// In future, we should have a separate field for executable name and it's arguments
fn process_command(&self, install_dir: &String, raw_command: String) -> (PathBuf, Vec<String>) {
// let command_components = raw_command.split(" ").collect::<Vec<&str>>();
let root = raw_command;
fn process_command(&self, install_dir: &String, command: Vec<String>) -> (PathBuf, Vec<String>) {
let root = &command[0];
let install_dir = Path::new(install_dir);
let absolute_exe = install_dir.join(root);
@ -79,34 +80,40 @@ impl ProcessManager<'_> {
*/
(absolute_exe, Vec::new())
}
pub fn kill_game(&mut self, meta: DownloadableMetadata) -> Result<(), io::Error> {
return match self.processes.get(&meta) {
pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
match self.processes.get(&game_id) {
Some(child) => {
child.kill()?;
child.wait()?;
Ok(())
},
}
None => Err(io::Error::new(
io::ErrorKind::NotFound,
"Game ID not running",
)),
};
}
}
fn on_process_finish(&mut self, meta: DownloadableMetadata, result: Result<ExitStatus, std::io::Error>) {
if !self.processes.contains_key(&meta) {
fn on_process_finish(&mut self, game_id: String, result: Result<ExitStatus, std::io::Error>) {
if !self.processes.contains_key(&game_id) {
warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result);
return;
}
info!("process for {:?} exited with {:?}", meta, result);
debug!("process for {:?} exited with {:?}", &game_id, result);
self.processes.remove(&meta);
self.processes.remove(&game_id);
let mut db_handle = DB.borrow_data_mut().unwrap();
let mut db_handle = borrow_db_mut_checked();
let meta = db_handle
.applications
.installed_game_version
.get(&game_id)
.cloned()
.unwrap();
db_handle.applications.transient_statuses.remove(&meta);
let current_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
if let Some(saved_state) = current_state {
if let GameDownloadStatus::SetupRequired {
version_name,
@ -116,7 +123,7 @@ impl ProcessManager<'_> {
if let Ok(exit_code) = result {
if exit_code.success() {
db_handle.applications.game_statuses.insert(
meta.id.clone(),
game_id.clone(),
GameDownloadStatus::Installed {
version_name: version_name.to_string(),
install_dir: install_dir.to_string(),
@ -128,9 +135,9 @@ impl ProcessManager<'_> {
}
drop(db_handle);
let status = GameStatusManager::fetch_state(&meta.id);
let status = GameStatusManager::fetch_state(&game_id);
push_game_update(&self.app_handle, &meta, status);
push_game_update(&self.app_handle, &game_id, status);
// TODO better management
}
@ -142,59 +149,86 @@ impl ProcessManager<'_> {
.contains_key(&(current.clone(), platform.clone())))
}
pub fn launch_process(&mut self, meta: DownloadableMetadata) -> Result<(), String> {
if self.processes.contains_key(&meta) {
return Err("Game or setup is already running.".to_owned());
pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
if self.processes.contains_key(&game_id) {
return Err(ProcessError::AlreadyRunning);
}
let mut db_lock = DB.borrow_data_mut().unwrap();
info!("Launching process {:?} with games {:?}", meta, db_lock.applications.game_versions);
let version = match DB
.borrow_data()
.unwrap()
.applications
.game_statuses
.get(&game_id)
.cloned()
{
Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
Some(GameDownloadStatus::SetupRequired { .. }) => {
return Err(ProcessError::SetupRequired)
}
_ => return Err(ProcessError::NotInstalled),
};
let meta = DownloadableMetadata {
id: game_id.clone(),
version: Some(version.clone()),
download_type: DownloadType::Game,
};
let mut db_lock = borrow_db_mut_checked();
debug!(
"Launching process {:?} with games {:?}",
&game_id, db_lock.applications.game_versions
);
let game_status = db_lock
.applications
.game_statuses
.get(&meta.id)
.ok_or("Game not installed")?;
.get(&game_id)
.ok_or(ProcessError::NotInstalled)?;
let status_metadata: Option<(&String, &String)> = match game_status {
let (version_name, install_dir) = match game_status {
GameDownloadStatus::Installed {
version_name,
install_dir,
} => Some((version_name, install_dir)),
} => (version_name, install_dir),
GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Some((version_name, install_dir)),
_ => None,
} => (version_name, install_dir),
_ => return Err(ProcessError::NotDownloaded),
};
if status_metadata.is_none() {
return Err("Game has not been downloaded.".to_owned());
}
let (version_name, install_dir) = status_metadata.unwrap();
let game_version = db_lock
.applications
.game_versions
.get(&meta.id)
.ok_or("Invalid game ID".to_owned())?
.get(&game_id)
.ok_or(ProcessError::InvalidID)?
.get(version_name)
.ok_or("Invalid version name".to_owned())?;
.ok_or(ProcessError::InvalidVersion)?;
let raw_command: String = match game_status {
let mut command: Vec<String> = Vec::new();
match game_status {
GameDownloadStatus::Installed {
version_name: _,
install_dir: _,
} => game_version.launch_command.clone(),
} => {
command.extend([game_version.launch_command.clone()]);
command.extend(game_version.launch_args.clone());
},
GameDownloadStatus::SetupRequired {
version_name: _,
install_dir: _,
} => game_version.setup_command.clone(),
} => {
command.extend([game_version.setup_command.clone()]);
command.extend(game_version.setup_args.clone());
},
_ => panic!("unreachable code"),
};
info!("Command: {:?}", &command);
let (command, args) = self.process_command(install_dir, raw_command);
let (command, args) = self.process_command(install_dir, command);
let target_current_dir = command.parent().unwrap().to_str().unwrap();
@ -210,11 +244,13 @@ impl ProcessManager<'_> {
.truncate(true)
.read(true)
.create(true)
.open(
self.log_output_dir
.join(format!("{}-{}-{}.log", meta.id.clone(), meta.version.clone().unwrap_or_default(), current_time.timestamp())),
)
.map_err(|v| v.to_string())?;
.open(self.log_output_dir.join(format!(
"{}-{}-{}.log",
&game_id,
&version,
current_time.timestamp()
)))
.map_err(ProcessError::IOError)?;
let error_file = OpenOptions::new()
.write(true)
@ -223,11 +259,11 @@ impl ProcessManager<'_> {
.create(true)
.open(self.log_output_dir.join(format!(
"{}-{}-{}-error.log",
meta.id.clone(),
meta.version.clone().unwrap_or_default(),
&game_id,
&version,
current_time.timestamp()
)))
.map_err(|v| v.to_string())?;
.map_err(ProcessError::IOError)?;
let current_platform = self.current_platform.clone();
let target_platform = game_version.platform.clone();
@ -235,20 +271,21 @@ impl ProcessManager<'_> {
let game_launcher = self
.game_launchers
.get(&(current_platform, target_platform))
.ok_or("Invalid version for this platform.")
.map_err(|e| e.to_string())?;
.ok_or(ProcessError::InvalidPlatform)?;
let launch_process = game_launcher.launch_process(
&meta,
command.to_str().unwrap().to_owned(),
args,
&target_current_dir.to_string(),
log_file,
error_file,
)?;
let launch_process = game_launcher
.launch_process(
&meta,
command.to_string_lossy().to_string(),
game_version,
target_current_dir,
log_file,
error_file,
)
.map_err(ProcessError::IOError)?;
let launch_process_handle =
Arc::new(SharedChild::new(launch_process).map_err(|e| e.to_string())?);
Arc::new(SharedChild::new(launch_process).map_err(ProcessError::IOError)?);
db_lock
.applications
@ -257,7 +294,7 @@ impl ProcessManager<'_> {
push_game_update(
&self.app_handle,
&meta,
&meta.id,
(None, Some(ApplicationTransientStatus::Running {})),
);
@ -272,7 +309,7 @@ impl ProcessManager<'_> {
let app_state_handle = app_state.lock().unwrap();
let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
process_manager_handle.on_process_finish(wait_thread_game_id, result);
process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
// As everything goes out of scope, they should get dropped
// But just to explicit about it
@ -280,10 +317,7 @@ impl ProcessManager<'_> {
drop(app_state_handle);
});
self.processes.insert(meta, wait_thread_handle);
info!("finished spawning process");
self.processes.insert(meta.id, wait_thread_handle);
Ok(())
}
}
@ -298,32 +332,31 @@ pub trait ProcessHandler: Send + 'static {
fn launch_process(
&self,
meta: &DownloadableMetadata,
command: String,
args: Vec<String>,
current_dir: &String,
launch_command: String,
game_version: &GameVersion,
current_dir: &str,
log_file: File,
error_file: File,
) -> Result<Child, String>;
) -> Result<Child, Error>;
}
struct NativeGameLauncher;
impl ProcessHandler for NativeGameLauncher {
fn launch_process(
&self,
meta: &DownloadableMetadata,
command: String,
args: Vec<String>,
current_dir: &String,
_meta: &DownloadableMetadata,
launch_command: String,
game_version: &GameVersion,
current_dir: &str,
log_file: File,
error_file: File,
) -> Result<Child, String> {
Command::new(command)
) -> Result<Child, Error> {
Command::new(PathBuf::from(launch_command))
.current_dir(current_dir)
.stdout(log_file)
.stderr(error_file)
.args(args)
.args(game_version.launch_args.clone())
.spawn()
.map_err(|v| v.to_string())
}
}
@ -332,18 +365,23 @@ struct UMULauncher;
impl ProcessHandler for UMULauncher {
fn launch_process(
&self,
meta: &DownloadableMetadata,
command: String,
args: Vec<String>,
current_dir: &String,
log_file: File,
error_file: File,
) -> Result<Child, String> {
UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command)
.game_id(String::from("0"))
.launch_args(args)
_meta: &DownloadableMetadata,
launch_command: String,
game_version: &GameVersion,
_current_dir: &str,
_log_file: File,
_error_file: File,
) -> Result<Child, Error> {
println!("Game override: .{:?}.", &game_version.umu_id_override);
let game_id = match &game_version.umu_id_override {
Some(game_override) => game_override.is_empty().then_some(game_version.game_id.clone()).unwrap_or(game_override.clone()) ,
None => game_version.game_id.clone()
};
info!("Game ID: {}", game_id);
UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, launch_command)
.game_id(game_id)
.launch_args(game_version.launch_args.clone())
.build()
.spawn()
.map_err(|x| x.to_string())
}
}

View file

@ -1,156 +0,0 @@
use std::{
error::Error,
fmt::{Display, Formatter},
sync::{Arc, Mutex},
};
use http::StatusCode;
use log::{info, warn};
use reqwest::blocking::Response;
use serde::Deserialize;
use url::{ParseError, Url};
use crate::{AppState, AppStatus, DB};
#[derive(Debug, Clone)]
pub enum RemoteAccessError {
FetchError(Arc<reqwest::Error>),
ParsingError(ParseError),
InvalidCodeError(u16),
InvalidEndpoint,
HandshakeFailed(String),
GameNotFound,
InvalidResponse(DropServerError),
InvalidRedirect,
ManifestDownloadFailed(StatusCode, String),
OutOfSync,
Generic(String),
}
impl Display for RemoteAccessError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RemoteAccessError::FetchError(error) => write!(
f,
"{}: {}",
error,
error
.source()
.map(|e| e.to_string())
.or_else(|| Some("Unknown error".to_string()))
.unwrap()
),
RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{}", parse_error)
}
RemoteAccessError::InvalidCodeError(error) => write!(f, "Invalid HTTP code {}", error),
RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
RemoteAccessError::HandshakeFailed(message) => write!(f, "Failed to complete handshake: {}", message),
RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"),
RemoteAccessError::InvalidResponse(error) => write!(f, "Server returned an invalid response: {} {}", error.status_code, error.status_message),
RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"),
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
f,
"Failed to download game manifest: {} {}",
status, response
),
RemoteAccessError::OutOfSync => write!(f, "Server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other."),
RemoteAccessError::Generic(message) => write!(f, "{}", message),
}
}
}
impl From<reqwest::Error> for RemoteAccessError {
fn from(err: reqwest::Error) -> Self {
RemoteAccessError::FetchError(Arc::new(err))
}
}
impl From<ParseError> for RemoteAccessError {
fn from(err: ParseError) -> Self {
RemoteAccessError::ParsingError(err)
}
}
impl From<u16> for RemoteAccessError {
fn from(err: u16) -> Self {
RemoteAccessError::InvalidCodeError(err)
}
}
impl std::error::Error for RemoteAccessError {}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DropServerError {
pub status_code: usize,
pub status_message: String,
pub message: String,
pub url: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DropHealthcheck {
app_name: String,
}
async fn use_remote_logic<'a>(
url: String,
state: tauri::State<'_, Mutex<AppState<'a>>>,
) -> Result<(), RemoteAccessError> {
info!("connecting to url {}", url);
let base_url = Url::parse(&url)?;
// Test Drop url
let test_endpoint = base_url.join("/api/v1")?;
let response = reqwest::get(test_endpoint.to_string()).await?;
let result = response.json::<DropHealthcheck>().await?;
if result.app_name != "Drop" {
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
return Err(RemoteAccessError::InvalidEndpoint);
}
let mut app_state = state.lock().unwrap();
app_state.status = AppStatus::SignedOut;
drop(app_state);
let mut db_state = DB.borrow_data_mut().unwrap();
db_state.base_url = base_url.to_string();
drop(db_state);
DB.save().unwrap();
Ok(())
}
#[tauri::command]
pub async fn use_remote<'a>(
url: String,
state: tauri::State<'_, Mutex<AppState<'a>>>,
) -> Result<(), String> {
let result = use_remote_logic(url, state).await;
if result.is_err() {
return Err(result.err().unwrap().to_string());
}
Ok(())
}
#[tauri::command]
pub fn gen_drop_url(path: String) -> Result<String, String> {
let base_url = {
let handle = DB.borrow_data().unwrap();
if handle.base_url.is_empty() {
return Ok("".to_string());
};
Url::parse(&handle.base_url).unwrap()
};
let url = base_url.join(&path).unwrap();
Ok(url.to_string())
}

View file

@ -1,18 +1,22 @@
use std::{env, sync::Mutex};
use chrono::Utc;
use log::{info, warn};
use log::{debug, error, warn};
use openssl::{ec::EcKey, hash::MessageDigest, pkey::PKey, sign::Signer};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
db::{DatabaseAuth, DatabaseImpls},
remote::{DropServerError, RemoteAccessError},
database::db::{
borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls,
},
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppState, AppStatus, User, DB,
};
use super::requests::make_request;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct InitiateRequestBody {
@ -35,6 +39,7 @@ struct HandshakeResponse {
id: String,
}
// TODO: Change return value on Err
pub fn sign_nonce(private_key: String, nonce: String) -> Result<String, ()> {
let client_private_key = EcKey::private_key_from_pem(private_key.as_bytes()).unwrap();
let pkey_private_key = PKey::from_ec_key(client_private_key).unwrap();
@ -50,7 +55,7 @@ pub fn sign_nonce(private_key: String, nonce: String) -> Result<String, ()> {
pub fn generate_authorization_header() -> String {
let certs = {
let db = DB.borrow_data().unwrap();
let db = borrow_db_checked();
db.auth.clone().unwrap()
};
@ -64,29 +69,25 @@ pub fn generate_authorization_header() -> String {
pub fn fetch_user() -> Result<User, RemoteAccessError> {
let base_url = DB.fetch_base_url();
let endpoint = base_url.join("/api/v1/client/user")?;
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
f.header("Authorization", header)
})?
.send()?;
if response.status() != 200 {
let data = response.json::<DropServerError>()?;
info!("Could not fetch user: {}", data.status_message);
let err: DropServerError = response.json()?;
warn!("{:?}", err);
if data.status_message == "Nonce expired" {
if err.status_message == "Nonce expired" {
return Err(RemoteAccessError::OutOfSync);
}
return Err(RemoteAccessError::InvalidCodeError(0));
return Err(RemoteAccessError::InvalidResponse(err));
}
let user = response.json::<User>()?;
Ok(user)
response.json::<User>().map_err(|e| e.into())
}
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
@ -99,7 +100,7 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
}
let base_url = {
let handle = DB.borrow_data().unwrap();
let handle = borrow_db_checked();
Url::parse(handle.base_url.as_str())?
};
@ -113,18 +114,18 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
let client = reqwest::blocking::Client::new();
let response = client.post(endpoint).json(&body).send()?;
info!("{}", response.status().as_u16());
let response_struct = response.json::<HandshakeResponse>()?;
debug!("handshake responsded with {}", response.status().as_u16());
let response_struct: HandshakeResponse = response.json()?;
{
let mut handle = DB.borrow_data_mut().unwrap();
let mut handle = borrow_db_mut_checked();
handle.auth = Some(DatabaseAuth {
private: response_struct.private,
cert: response_struct.certificate,
client_id: response_struct.id,
});
drop(handle);
DB.save().unwrap();
save_db();
}
{
@ -137,12 +138,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
Ok(())
}
#[tauri::command]
pub fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), String> {
recieve_handshake(app, format!("handshake/{}", token));
Ok(())
}
pub fn recieve_handshake(app: AppHandle, path: String) {
// Tell the app we're processing
app.emit("auth/processing", ()).unwrap();
@ -157,9 +152,9 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
app.emit("auth/finished", ()).unwrap();
}
fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
let base_url = {
let db_lock = DB.borrow_data().unwrap();
let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())?
};
@ -173,8 +168,8 @@ fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
let response = client.post(endpoint.to_string()).json(&body).send()?;
if response.status() != 200 {
let data = response.json::<DropServerError>()?;
info!("Could not start handshake: {}", data.status_message);
let data: DropServerError = response.json()?;
error!("could not start handshake: {}", data.status_message);
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
}
@ -182,79 +177,25 @@ fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
let redir_url = response.text()?;
let complete_redir_url = base_url.join(&redir_url)?;
info!("opening web browser to continue authentication");
debug!("opening web browser to continue authentication");
webbrowser::open(complete_redir_url.as_ref()).unwrap();
Ok(())
}
#[tauri::command]
pub fn auth_initiate<'a>() -> Result<(), String> {
let result = auth_initiate_wrapper();
if result.is_err() {
return Err(result.err().unwrap().to_string());
}
Ok(())
}
#[tauri::command]
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> Result<(), ()> {
let (app_status, user) = setup()?;
let mut guard = state.lock().unwrap();
guard.status = app_status;
guard.user = user;
drop(guard);
Ok(())
}
pub fn setup() -> Result<(AppStatus, Option<User>), ()> {
let data = DB.borrow_data().unwrap();
pub fn setup() -> (AppStatus, Option<User>) {
let data = borrow_db_checked();
let auth = data.auth.clone();
drop(data);
if auth.is_some() {
let user_result = fetch_user();
if user_result.is_err() {
let error = user_result.err().unwrap();
warn!("auth setup failed with: {}", error);
match error {
RemoteAccessError::FetchError(_) => {
return Ok((AppStatus::ServerUnavailable, None))
}
_ => return Ok((AppStatus::SignedInNeedsReauth, None)),
}
}
return Ok((AppStatus::SignedIn, Some(user_result.unwrap())));
let user_result = match fetch_user() {
Ok(data) => data,
Err(RemoteAccessError::FetchError(_)) => return (AppStatus::ServerUnavailable, None),
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
};
return (AppStatus::SignedIn, Some(user_result));
}
Ok((AppStatus::SignedOut, None))
}
#[tauri::command]
pub fn sign_out(app: AppHandle) -> Result<(), String> {
info!("Signing out user");
// Clear auth from database
{
let mut handle = DB.borrow_data_mut().unwrap();
handle.auth = None;
drop(handle);
DB.save().unwrap();
}
// Update app state
{
let app_state = app.state::<Mutex<AppState>>();
let mut app_state_handle = app_state.lock().unwrap();
app_state_handle.status = AppStatus::SignedOut;
app_state_handle.user = None;
}
// Emit event for frontend
app.emit("auth/signedout", ()).unwrap();
Ok(())
(AppStatus::SignedOut, None)
}

View file

@ -0,0 +1,78 @@
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db},
error::remote_access_error::RemoteAccessError,
AppState, AppStatus,
};
use super::{
auth::{auth_initiate_logic, recieve_handshake, setup},
remote::use_remote_logic,
};
#[tauri::command]
pub fn use_remote(
url: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<(), RemoteAccessError> {
use_remote_logic(url, state)
}
#[tauri::command]
pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
let base_url = {
let handle = borrow_db_checked();
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
};
let url = base_url.join(&path).unwrap();
Ok(url.to_string())
}
#[tauri::command]
pub fn sign_out(app: AppHandle) {
// Clear auth from database
{
let mut handle = borrow_db_mut_checked();
handle.auth = None;
drop(handle);
save_db();
}
// Update app state
{
let app_state = app.state::<Mutex<AppState>>();
let mut app_state_handle = app_state.lock().unwrap();
app_state_handle.status = AppStatus::SignedOut;
app_state_handle.user = None;
}
// Emit event for frontend
app.emit("auth/signedout", ()).unwrap();
}
#[tauri::command]
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) {
let (app_status, user) = setup();
let mut guard = state.lock().unwrap();
guard.status = app_status;
guard.user = user;
drop(guard);
}
#[tauri::command]
pub fn auth_initiate() -> Result<(), RemoteAccessError> {
auth_initiate_logic()
}
#[tauri::command]
pub fn manual_recieve_handshake(app: AppHandle, token: String) {
recieve_handshake(app, format!("handshake/{}", token));
}

View file

@ -0,0 +1,4 @@
pub mod auth;
pub mod commands;
pub mod remote;
pub mod requests;

View file

@ -0,0 +1,48 @@
use std::sync::Mutex;
use log::{debug, warn};
use serde::Deserialize;
use url::Url;
use crate::{
database::db::{borrow_db_mut_checked, save_db},
error::remote_access_error::RemoteAccessError,
AppState, AppStatus,
};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DropHealthcheck {
app_name: String,
}
pub fn use_remote_logic(
url: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<(), RemoteAccessError> {
debug!("connecting to url {}", url);
let base_url = Url::parse(&url)?;
// Test Drop url
let test_endpoint = base_url.join("/api/v1")?;
let response = reqwest::blocking::get(test_endpoint.to_string())?;
let result: DropHealthcheck = response.json()?;
if result.app_name != "Drop" {
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
return Err(RemoteAccessError::InvalidEndpoint);
}
let mut app_state = state.lock().unwrap();
app_state.status = AppStatus::SignedOut;
drop(app_state);
let mut db_state = borrow_db_mut_checked();
db_state.base_url = base_url.to_string();
drop(db_state);
save_db();
Ok(())
}

View file

@ -0,0 +1,23 @@
use reqwest::blocking::{Client, RequestBuilder};
use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB};
pub fn make_request<T: AsRef<str>, F: FnOnce(RequestBuilder) -> RequestBuilder>(
client: &Client,
path_components: &[T],
query: &[(T, T)],
f: F,
) -> Result<RequestBuilder, RemoteAccessError> {
let mut base_url = DB.fetch_base_url();
for endpoint in path_components {
base_url = base_url.join(endpoint.as_ref())?;
}
{
let mut queries = base_url.query_pairs_mut();
for (param, val) in query {
queries.append_pair(param.as_ref(), val.as_ref());
}
}
let response = client.get(base_url);
Ok(f(response))
}

View file

@ -1 +0,0 @@
mod progress_tests;

View file

@ -1,29 +0,0 @@
/*
use atomic_counter::RelaxedCounter;
use crate::downloads::progress::ProgressChecker;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
#[test]
fn test_progress_sequentially() {
let counter = Arc::new(RelaxedCounter::new(0));
let callback = Arc::new(AtomicBool::new(false));
let p = ProgressChecker::new(Box::new(test_fn), counter.clone(), callback, 100);
p.run_contexts_sequentially((1..100).collect());
println!("Progress: {}", p.get_progress_percentage());
}
#[test]
fn test_progress_parallel() {
let counter = Arc::new(RelaxedCounter::new(0));
let callback = Arc::new(AtomicBool::new(false));
let p = ProgressChecker::new(Box::new(test_fn), counter.clone(), callback, 100);
p.run_contexts_parallel_background((1..100).collect(), 10);
}
fn test_fn(int: usize, _callback: Arc<AtomicBool>, _counter: Arc<RelaxedCounter>) {
println!("{}", int);
}
*/

View file

@ -1,3 +0,0 @@
pub struct CompatibilityLayer {
}

View file

@ -1,4 +0,0 @@
mod prefix;
mod registry;
mod tool;
mod compatibility_layer;

View file

@ -1,7 +0,0 @@
use std::collections::HashMap;
use crate::download_manager::downloadable::Downloadable;
pub struct Registry<T: Downloadable> {
tools: HashMap<String, T>
}

View file

@ -1,54 +0,0 @@
use std::sync::Arc;
use tauri::AppHandle;
use crate::download_manager::{application_download_error::ApplicationDownloadError, download_thread_control_flag::DownloadThreadControl, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject};
pub struct ToolDownloadAgent {
id: String,
version: String,
location: String,
control_flag: DownloadThreadControl,
progress: Arc<ProgressObject>,
}
impl Downloadable for ToolDownloadAgent {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError> {
todo!()
}
fn progress(&self) -> Arc<ProgressObject> {
todo!()
}
fn control_flag(&self) -> DownloadThreadControl {
todo!()
}
fn status(&self) -> crate::download_manager::download_manager::DownloadStatus {
todo!()
}
fn metadata(&self) -> DownloadableMetadata {
todo!()
}
fn on_initialised(&self, app_handle: &tauri::AppHandle) {
todo!()
}
fn on_error(&self, app_handle: &tauri::AppHandle, error: crate::download_manager::application_download_error::ApplicationDownloadError) {
todo!()
}
fn on_complete(&self, app_handle: &tauri::AppHandle) {
todo!()
}
fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
todo!()
}
fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
todo!()
}
}

View file

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Drop Desktop Client",
"version": "0.1.0",
"version": "0.2.0-beta",
"identifier": "dev.drop.app",
"build": {
"beforeDevCommand": "yarn dev --port 1432",
@ -23,7 +23,7 @@
},
"bundle": {
"active": true,
"targets": ["nsis", "deb", "rpm", "dmg"],
"targets": ["nsis", "deb", "rpm", "dmg", "appimage"],
"windows": {
"nsis": {
"installMode": "both"

View file

@ -71,4 +71,9 @@ export type DownloadableMetadata = {
id: string,
version: string,
downloadType: DownloadableType
}
export type Settings = {
autostart: boolean,
maxDownloadThreads: number,
}