Compare commits

...

191 commits
1.5 ... master

Author SHA1 Message Date
liushuyu f46fed17b7
Merge pull request #29 from liushuyu/yuzu
Installer Overhaul
2023-01-03 18:32:25 -07:00
liushuyu 31b3e7e3a6
CI: use Node.js 16 LTS 2022-12-24 18:51:41 -07:00
liushuyu fec5587b87
ui/authenticationview: fix token pasting ...
... on certain platforms where current context does not include
Clipboard API in the navigator namespace
2022-12-24 17:38:34 -07:00
liushuyu d28b19c25a
deps: remove babel-eslint ...
... also update dependencies to their latest LTS version
2022-12-24 17:27:14 -07:00
liushuyu 08ab3c369e
ui: fix authentication view indentation error 2022-11-27 17:20:12 -07:00
liushuyu d8df3b3114 Merge lat9nq's changes 2022-11-27 17:11:03 -07:00
liushuyu 61a7db2005
fix(auth): fix JWT verification 2022-11-27 17:02:05 -07:00
liushuyu 7b8bf579f2
ui: update dependencies 2022-11-27 16:49:44 -07:00
liushuyu 3ea6aa9852
deps: update dependencies 2022-11-27 16:49:13 -07:00
bunnei faeb4885bf
Merge pull request #30 from lat9nq/linux-mainline-fix
linux v3: Use AppImage for Mainline
2022-08-01 11:17:04 -07:00
lat9nq 85ed6275f8 linux v3: Use AppImage for Mainline
The regular yuzu executable is not anywhere near as guaranteed to run as
the AppImage is.
2022-07-31 19:47:29 -04:00
liushuyu f3d0d06a09
Merge pull request #28 from lat9nq/ea-linux
general: Fix Linux build
2022-07-28 17:13:26 -06:00
lat9nq 8c795396eb installer: Use an inline expression for is_windows
Co-authored-by: liushuyu <liushuyu011@gmail.com>
2022-07-28 16:57:36 -04:00
lat9nq e6600e3b17 general: Housekeeping
Keep up with updates to the compiler.
2022-07-28 16:39:35 -04:00
liushuyu 89be1c0d84
fix(tasks): fix shortcut manipulation logic 2022-04-02 22:03:53 -06:00
liushuyu 42e092f54d
feat(ui): add a "back" button ...
... on the complete view when entered in maintenance mode
2022-04-01 20:27:04 -06:00
liushuyu 278af40b37
feat(ui): add underline when hovering on a clickable link 2022-04-01 20:15:24 -06:00
liushuyu 8917ba88ca
fix(auth): fix panic issue when frontend returned an invalid payload 2022-04-01 20:13:04 -06:00
liushuyu b87dab83d8
feat(ui/views): replace primary selection buttons ...
... with accent colors
2022-04-01 18:59:13 -06:00
liushuyu 79b799f655
feat(ui/selectpackages): make the cursor a pointer shape ...
... when interacting with the selection box
2022-04-01 18:54:34 -06:00
liushuyu c61c068ed0
feat(ui): lint js code 2022-03-31 03:41:39 -06:00
liushuyu 3727e4185b
fix(tasks): fix multiple logic issues under Windows 2022-03-31 02:10:25 -06:00
liushuyu d6cb916a9c
ui: sync translations 2022-03-30 16:26:09 -06:00
liushuyu d269677b2c
tree-wide: format code 2022-03-30 01:47:24 -06:00
liushuyu 9a27b24f05
meta: affix an icon to the title bar 2022-03-30 01:38:46 -06:00
liushuyu 3fc8583646
tasks: uninstall the program as clean as possible 2022-03-30 00:30:25 -06:00
liushuyu a3f0d0f999
ui: adjust repair warning prompt layout 2022-03-29 23:20:37 -06:00
liushuyu d194ed5dd5
tasks: fix shortcut creation logic 2022-03-29 23:19:40 -06:00
liushuyu 0decda8232
ui: fix clickable box contrast issue under darkmode 2022-03-29 22:23:14 -06:00
liushuyu 2103a8ec15
ui: warn about file deletion before a repair 2022-03-29 22:22:20 -06:00
liushuyu a86bd209a8
locales: add German translations 2022-03-29 18:51:57 -06:00
liushuyu eff81e6d99
ui/app: make language dropdown scrollable 2022-03-28 21:48:34 -06:00
liushuyu d63473ec9c
locales: update translations 2022-03-28 21:20:15 -06:00
liushuyu 1fd97b6e42
ui: update dependencies (LTS versions) 2022-03-28 17:09:40 -06:00
liushuyu 0cfa44330d
deps: update dependencies 2022-03-28 17:02:46 -06:00
liushuyu 679312f101
fix(CI): fix Windows CI 2021-10-15 23:37:41 -06:00
liushuyu 2120abf299
fix(ci): fix CI 2021-10-15 19:10:08 -06:00
liushuyu a8db5ff8c4
fix(ui/views): fix EA auth UI handling 2021-10-15 19:09:09 -06:00
liushuyu faba49c025 feat(frontend/win): bundle webview2 installer 2021-10-15 19:09:02 -06:00
liushuyu 3196736d36 fix(frontend): fix early access authentication 2021-10-15 19:09:02 -06:00
lat9nq f809e6cb23
Update src/native/mod.rs
Co-authored-by: liushuyu <liushuyu011@gmail.com>
2021-10-15 19:19:00 -04:00
lat9nq 77a26c1496
Update src/tasks/install_desktop_shortcut.rs
Co-authored-by: liushuyu <liushuyu011@gmail.com>
2021-10-15 19:18:56 -04:00
liushuyu e990138200
fix(ui): fix fractional scaling 2021-10-15 14:37:18 -06:00
liushuyu 6e7d045794
feat(ui): migrate UI/Web framework to WRY 2021-10-15 04:35:47 -06:00
liushuyu 0d4022d348
feat(install): add recovery mode ...
... when metadata is corrupted, recovery mode will be activated
2021-10-15 01:42:47 -06:00
liushuyu 109322836b
feat(ui): add a mock to mock authentication ...
... also updates UI/JS dependencies
2021-10-15 00:28:45 -06:00
liushuyu fbf7640657
deps: update dependencies 2021-10-14 23:45:30 -06:00
liushuyu 2d42189e5e
feat(ui/i18n): mark most of the yuzu-specific strings as translatable 2021-10-14 23:03:21 -06:00
liushuyu dde96db57c
fix(tree-wide): re-apply yuzu specific changes 2021-10-14 21:13:31 -06:00
liushuyu a816cbe767 Merge remote-tracking branch 'fix-usability' into yuzu 2021-07-28 18:18:38 -06:00
lat9nq 95ee7a1739 native: Linux shortcut specific to maintenance tool 2021-07-25 16:18:27 -04:00
lat9nq 061944079b travis: Build using the linux-liftinstall Docker container
travis: Build in release mode

travis: Use yuzu user for building

travis: Fix permissions resetting
2021-07-24 19:02:34 -04:00
lat9nq e54199ad6f SelectPackages: Desktop shortcut is for Windows 2021-07-24 19:02:28 -04:00
lat9nq 4ed1ffb5c3 general: Get ready for Linux release
Creates a config for Linux based on config.windows.v10.toml, then points
to it. We also remove the icon stub.
2021-07-24 19:02:28 -04:00
lat9nq 2958c583af views: Stub things that don't work or don't apply to Linux
Icons don't work in Linux. Start menu is not a Linux thing.
Automatically scrolls to the verify token button in the Authentication
view.
2021-07-24 19:02:28 -04:00
lat9nq 825e9cc1c3 general: Fix Linux shortcuts
Makes them function even if it's missing the icon.
2021-07-24 19:02:28 -04:00
lat9nq 810ef5fb25 src: Fix Linux build
Syncs the parameters between the Windows and Linux create_shortcut
functions. Makes the install_desktop_shortcut only work on Windows where
it has create_desktop_shortcut implemented.
2021-07-24 19:02:15 -04:00
liushuyu 89e1b2f91f
ui: web-view: set debug mode on debug build 2021-06-08 18:05:38 -06:00
liushuyu f13b2fe93d
deps: update dependencies...
... and use tinyfiledialog instead of the web-view provided one
2021-06-08 17:42:43 -06:00
liushuyu df0414b26e
ui: fix eslint errors 2021-03-21 21:59:28 -06:00
liushuyu a9de893cca
ui: show error if no package is selected 2021-03-21 21:38:32 -06:00
liushuyu bdda585f12
deps: update dependencies 2021-03-21 21:38:10 -06:00
liushuyu 27aa9924f3 ui/ux: implement "View Local Files" function...
... for Windows
2020-11-06 17:14:34 -07:00
liushuyu 322f72609f ui/ux: implement "View Local Files" function...
... for Linux/macOS
2020-07-19 19:01:23 -06:00
liushuyu ca994e49d3 ui: bugfix: do not show the overwrite dialog...
... when doing a repair
2020-07-19 19:01:23 -06:00
liushuyu 41918c709c frontend/rest/assets: add missing webfonts for icons 2020-07-19 19:01:23 -06:00
liushuyu 48fa172169 meta: refine i18n and mark more strings 2020-07-19 19:01:12 -06:00
liushuyu 928661db77 ux: allow user to overwrite the directory...
... if they really want
2020-07-19 18:46:35 -06:00
liushuyu 45c562d723 meta: add repair functionality 2020-07-19 18:46:32 -06:00
liushuyu c7628c1474 i18n: merge-strings: fix issues with only one locale 2020-07-18 14:09:19 +10:00
liushuyu 34fd140a9e ui: main: more robust error handling in /app/exit 2020-07-18 14:09:19 +10:00
liushuyu cd7fb8de28 ui: update NPM dependencies 2020-07-18 14:09:19 +10:00
liushuyu 01419e5da4 ui: mock-server: can now simulate other modes...
... like maintenance mode and launcher mode
2020-07-18 14:09:19 +10:00
James 3ce4504a5b
Create SECURITY.md 2020-05-29 16:01:03 +10:00
liushuyu 5003edd43d ui: messages: fix end of file line-ending 2020-05-29 15:57:05 +10:00
liushuyu 87efd394a1 use actual buttons 2020-05-29 15:57:05 +10:00
liushuyu 8e212460d8 helper: clean up functions 2020-05-29 15:57:05 +10:00
liushuyu 4bb84d98b3 DownloadConfig: fix error message display 2020-05-29 15:57:05 +10:00
liushuyu 1dbf078728 i18n: automated locale detection and...
... manual selection box
2020-05-29 15:57:05 +10:00
liushuyu 8e8d729019 mock-server: can now emulate errors 2020-05-29 15:57:05 +10:00
liushuyu 9fcfe0c77b ui: update frontend dependencies 2020-05-29 15:57:05 +10:00
liushuyu 74cefc277e i18n: basic implementation and string interpolation 2020-05-29 15:57:05 +10:00
liushuyu adbd4a304d ci: add caching 2020-05-29 15:56:05 +10:00
liushuyu 351be36f05 ci: switch to GitHub Actions completely 2020-05-29 15:56:05 +10:00
liushuyu 9866a32c10 deps: update dependencies 2020-05-28 16:50:52 +10:00
liushuyu 3537b5823f build.rs: fix build on Windows with gcc 2020-05-28 16:50:52 +10:00
liushuyu 5ff1486f69 meta: update to Rust 2018 standard 2020-05-28 16:50:52 +10:00
liushuyu 7acefbc8cb deps: update dependencies 2020-05-28 16:50:52 +10:00
bunnei ef71b707cb
Merge pull request #26 from jroweboy/replace-old
Change updater to replace the existing installer.
2019-12-30 13:19:19 -05:00
James Rowe c68ebcb61e Change updater to replace the existing installer. 2019-12-19 23:05:33 -07:00
bunnei 93e24ea06a
Merge pull request #25 from jroweboy/fix
Fix self update code path
2019-12-08 22:27:32 -05:00
James Rowe d9d8b92cc4 Fix self update code path 2019-12-08 20:12:54 -07:00
bunnei 76a77d3caf
Merge pull request #24 from jroweboy/patch-1
Unpublish update 1.8
2019-12-08 22:04:10 -05:00
James Rowe ea8b631aa2
Unpublish update 1.8 2019-12-08 20:03:23 -07:00
bunnei f848e8fb53
Merge pull request #23 from jroweboy/merged
Installer v1.8
2019-12-08 21:52:21 -05:00
James Rowe d9e4e5ecc2 Prevent fresh install with no packages selected 2019-12-08 17:26:42 -07:00
James Rowe 6210a2668f Attempt to refresh shortcuts on create 2019-12-08 17:26:18 -07:00
liushuyu d2399d97e4 Fix build on Linux and macOS 2019-12-08 06:07:40 +00:00
liushuyu b9e825faa5 ui/frontend: adaptive changes for...
... Vue and Webpack behavioral changes
2019-12-07 22:35:37 +00:00
liushuyu 630f2231ab ui: use axios as possible 2019-12-07 22:35:37 +00:00
liushuyu 91fb88aa98 use ajax to fetch basic_attrs 2019-12-07 22:35:37 +00:00
liushuyu 713b85b59a lint: automated lint 2019-12-07 22:35:37 +00:00
liushuyu ca6ac320c2 deps: update dependencies 2019-12-07 22:35:37 +00:00
James Rowe 9999c52ea8 Add a little padding to select packages description 2019-12-06 22:45:42 -07:00
James Rowe 6cae746192 Launch existing maintenance tool if it exists in the default install folder 2019-12-06 22:31:37 -07:00
James Rowe 732e344605 Launch app on fresh install exit. Add desktop shortcuts 2019-12-06 22:04:44 -07:00
James Rowe 9b58c273d1 Merge branches 'icon-swap' and 'right-click' into merged 2019-12-06 02:12:22 -07:00
James Rowe b356f0057f Dark theme logo and package icons 2019-12-06 02:11:49 -07:00
James Rowe d339816695 Use the correct icon for the application shortcuts 2019-12-06 09:09:15 +00:00
James Rowe d2ad619d87 Use the correct icon for the application shortcuts 2019-12-06 00:27:44 -07:00
James Rowe 128c1b1f41 Add paste button 2019-12-06 00:10:47 -07:00
James Rowe 37d27a82ba Move to installer v1.8 2019-12-05 09:49:03 -07:00
James Rowe eb556c8cab Force disable exp and nbf validation.
Some clients had the wrong time information so it would fail to validate their token when installing. Remove these checks since they'll be checked on the server side anyway
2019-12-02 09:38:33 -07:00
bunnei 6af46ec703
Merge pull request #18 from jroweboy/patreon-launch
Patreon launch
2019-12-01 16:47:12 -05:00
James Rowe db2176763d Update old config to the new one 2019-12-01 11:44:05 -07:00
bunnei fccd1c9bd2
Merge pull request #19 from j-selby/patreon-tweaking
Tweak Patreon authentication implementation
2019-12-01 13:40:26 -05:00
bunnei 0e190ecdc6
Update config.windows.v9.toml to point to early access 2019-11-26 21:27:23 -05:00
James f89cb19602 Mark 'req' as used in browser service 2019-11-16 05:51:42 +00:00
James 7392e1ef91 Tweak Patreon authentication implementation 2019-11-16 05:43:11 +00:00
bunnei 9cf5e745d4
Merge pull request #17 from jroweboy/patreon
Early access release
2019-11-14 17:26:37 -05:00
James Rowe de4246536e Update configs 2019-11-14 15:16:20 -07:00
James Rowe 30f817e4fa Add combined token support 2019-11-14 14:41:29 -07:00
James Rowe 6845ed9ad7 Fix crash when loading if the installer was updated from an older version 2019-11-07 10:56:50 -07:00
James Rowe 1639e74b98 Properly check for early access role in package select 2019-11-07 10:56:22 -07:00
James Rowe c176658e28 Update shortcut to the new folder
Also selects packages that require auth after successful authorization
2019-11-07 10:29:16 -07:00
James Rowe d79fd3e6e1 Change text chunk to a b-message 2019-11-04 21:33:27 -07:00
James Rowe d3c3b77e6b Various fixes to various things 2019-11-03 01:22:55 -06:00
James Rowe 561f0071bd Add is_new to config 2019-11-02 16:33:45 -06:00
James Rowe e72a5f0420 Change the link link to point to yuzu-emu.org 2019-11-01 11:44:07 -06:00
James Rowe 2b4b59320e Add authentication task dependency to check for auth on install 2019-11-01 11:15:16 -06:00
Flame Sage 004a49587c
Merge pull request #16 from jroweboy/revert-15-master
Revert "Change the default shortcut location to comply with Azure changes"
2019-10-24 18:55:44 +00:00
James Rowe d20c17964e
Revert "Change the default shortcut location to comply with Azure changes" 2019-10-24 12:50:40 -06:00
James Rowe 288518cd78 Minor fixes 2019-10-24 09:15:10 -06:00
Flame Sage fc40f6691c
Merge pull request #15 from Hexagon12/master
Change the default shortcut location to comply with Azure changes
2019-10-24 15:07:50 +00:00
Hexagon12 74cecab186
Changed the shortcut location to the new folder name 2019-10-24 17:05:54 +03:00
James Rowe 9bec77a2db Update bootstrap repo to point to my url 2019-10-21 01:32:45 -06:00
James Rowe 5409b32bf0 Add patreon authentication for early access releases 2019-10-21 01:11:57 -06:00
James Rowe a7057dfed3 Add new dependencies and update Cargo.lock 2019-10-21 01:11:57 -06:00
James Rowe c4b4c597fa Minimally compiling rust code for patreon release 2019-10-21 01:11:56 -06:00
bunnei 26997ba229
Merge pull request #14 from j-selby/patch-1
Move to tar.xz as the main distribution format
2019-10-08 22:35:08 -04:00
James 3bd85bac8d
Move to tar.xz as the main distribution format 2019-10-09 02:33:06 +00:00
Flame Sage 474fb71efd
Merge pull request #13 from j-selby/v8-rebase
Implement migration system to move from nightly/canary to mainline
2019-10-07 00:21:50 +00:00
James 548daa1b2b Migrate to new version 2019-10-07 01:11:36 +01:00
James bdbab4dc4d Implement migration system to move from canary/nightly 2019-10-07 01:08:56 +01:00
James ca8defda7e
Remove Ubuntu/macOS targets in Github Actions 2019-08-27 05:23:33 +00:00
James 6853ade29c
Fix checkout version 2019-08-27 05:20:53 +00:00
James b6122349d6
Remove un-needed name calls 2019-08-27 05:19:12 +00:00
James 12081db009
Delete main.workflow 2019-08-27 05:18:18 +00:00
James 3abc0a1b11
Add test Rust build script 2019-08-27 05:17:01 +00:00
James 56cdaabbae
Add GitHub Actions CI 2019-08-27 05:03:29 +00:00
dependabot[bot] a02e8a1624 Bump js-yaml from 3.13.0 to 3.13.1 in /ui
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.13.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.13.0...3.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-19 10:08:56 +00:00
dependabot[bot] 4b158036da Bump webpack-bundle-analyzer from 3.1.0 to 3.3.2 in /ui
Bumps [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) from 3.1.0 to 3.3.2.
- [Release notes](https://github.com/webpack-contrib/webpack-bundle-analyzer/releases)
- [Changelog](https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/webpack-bundle-analyzer/compare/v3.1.0...v3.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-19 10:08:46 +00:00
dependabot[bot] eb6475bac6 Bump lodash from 4.17.11 to 4.17.15 in /ui
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-19 08:05:21 +00:00
dependabot[bot] c7cef0b49d Bump lodash.defaultsdeep from 4.6.0 to 4.6.1 in /ui
Bumps [lodash.defaultsdeep](https://github.com/lodash/lodash) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.6.0...4.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-19 07:32:50 +00:00
James 2ee02bbf46
Assert return code is valid when running yarn 2019-07-19 06:26:06 +00:00
James 68109894f1 Update config files for v7 (#12)
* platform: fix build on Linux and update web-view

* deps: replace xz-decom with xz2 and update deps

* platform: fix regression...

... that prevents the build on Windows

* linux: implement platform-dependent functions

* travis: add macos and windows CI

* travis: use official Rust Docker image

* Update Cargo.lock for new version

* Break apart REST into separate services

This cleans up locking, ensures consistent futures for all endpoints
and enhances code re-use.

* Clean up codebase, fixing minor errors

* Update packages, use async client for downloading config

While this has a hell of a lot more boilerplate, this is quite
a bit cleaner.

* Add explicit 'dyn's as per Rust nightly requirements

* Migrate self updating functions to own module

* Migrate assets to server module

* Use patched web-view to fix dialogs, remove nfd

* Implement basic dark mode

* Revert window.close usage

* ui: split files and use Webpack

* frontend: ui: include prebuilt assets...

... and update rust side stuff

* build: integrate webpack building into build.rs

* Polish Vue UI split

* Add instructions for node + yarn

* native: fix uninstall self-destruction behavior...... by not showing the command prompt window and fork-spawning the cmd

* native: deal with Unicode issues in native APIs

* native: further improve Unicode support on Windows

* travis: add cache and fix issues

* ui: use Buefy components to...

... beautify the UI

* ui: makes error message selectable

* Make launcher mode behaviour more robust

* Fix error display on launcher pages

* Correctly handle exit on error

* Bump installer version
2019-07-04 21:23:16 -04:00
James 6d443805fc Bump version to 0.2.0 for new changes 2019-07-05 09:19:13 +10:00
James c4139f7e37 Correctly handle exit on error 2019-07-03 15:10:34 +10:00
James 6272c294c8 Fix error display on launcher pages 2019-07-03 14:40:39 +10:00
James 5d31fd0129 Merge branch 'ui-tweaks' into ui-tweaks-test 2019-07-03 14:10:37 +10:00
James e69443c22e Make launcher mode behaviour more robust 2019-07-03 14:09:07 +10:00
liushuyu c8699b6e62
ui: makes error message selectable 2019-07-01 10:01:02 +08:00
liushuyu e83cf6cf4e
ui: use Buefy components to...
... beautify the UI
2019-06-30 10:12:22 +08:00
liushuyu 9a28807423 travis: add cache and fix issues 2019-06-29 16:17:58 +00:00
liushuyu b3b686ed53 native: further improve Unicode support on Windows 2019-06-29 15:14:28 +00:00
liushuyu f80db92188 native: deal with Unicode issues in native APIs 2019-06-29 15:14:28 +00:00
liushuyu 4578450bff native: fix uninstall self-destruction behavior...... by not showing the command prompt window and fork-spawning the cmd 2019-06-29 15:14:28 +00:00
James 5603981af1 Add instructions for node + yarn 2019-06-26 23:44:17 +10:00
James 341a6a6537 Polish Vue UI split 2019-06-26 23:43:24 +10:00
liushuyu 27d0a05ade build: integrate webpack building into build.rs 2019-06-25 15:44:41 +08:00
liushuyu 6c19b8b0d1
frontend: ui: include prebuilt assets...
... and update rust side stuff
2019-06-25 12:45:56 +08:00
liushuyu ff574c9d73 ui: split files and use Webpack 2019-06-25 11:06:28 +08:00
James 761ce91299 Revert window.close usage 2019-06-24 00:44:18 +10:00
James 44e0ebdab4 Implement basic dark mode 2019-06-24 00:18:59 +10:00
James 270a17cd86 Use patched web-view to fix dialogs, remove nfd 2019-06-23 22:28:41 +10:00
James f24d1112dd Migrate assets to server module 2019-06-23 21:55:16 +10:00
James 3109d48dce Migrate self updating functions to own module 2019-06-23 21:46:04 +10:00
James 5d53ef7a2e Add explicit 'dyn's as per Rust nightly requirements 2019-06-23 21:35:41 +10:00
James 4d50a0f8f8 Update packages, use async client for downloading config
While this has a hell of a lot more boilerplate, this is quite
a bit cleaner.
2019-06-23 21:24:13 +10:00
James a447ef25b6 Clean up codebase, fixing minor errors 2019-06-23 20:27:35 +10:00
James 9d1f4c2576 Break apart REST into separate services
This cleans up locking, ensures consistent futures for all endpoints
and enhances code re-use.
2019-06-23 20:19:43 +10:00
James 30bb49e1fb Update Cargo.lock for new version 2019-06-20 22:36:35 +10:00
James 8b6c2c1708
Merge pull request #11 from liushuyu/fix-linux
linux: implement platform-dependent functions
2019-04-08 22:43:27 +10:00
James 0d63e0ab21
Merge pull request #10 from liushuyu/master
CI enhancements
2019-04-08 22:39:52 +10:00
liushuyu 269b083ec8
travis: use official Rust Docker image 2019-04-03 00:58:49 -06:00
liushuyu ae63bc7dab
travis: add macos and windows CI 2019-04-03 00:57:27 -06:00
liushuyu d3fb463f20
linux: implement platform-dependent functions 2019-04-01 13:50:02 -06:00
James 137d2ec539
Merge pull request #9 from liushuyu/fix-linux
Fix compile on Linux
2019-04-01 16:38:14 +11:00
liushuyu fed2d28aa8
platform: fix regression...
... that prevents the build on Windows
2019-03-29 11:44:26 -06:00
liushuyu 66e2473a40
deps: replace xz-decom with xz2 and update deps 2019-03-28 11:49:14 -06:00
liushuyu d236eeec0c
platform: fix build on Linux and update web-view 2019-03-28 11:08:13 -06:00
142 changed files with 16502 additions and 2895 deletions

58
.github/workflows/test-build.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Rust
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: hecrj/setup-rust-action@master
with:
rust-version: stable
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev libssl-dev libappindicator3-dev
if: runner.os == 'Linux'
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Get cargo cache directory path
id: cargo-cache-dir-path
run: echo "::set-output name=dir::$HOME/.cargo/"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- uses: actions/cache@v2
id: cargo-cache
with:
path: ${{ steps.cargo-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- uses: actions/setup-node@v1
with:
node-version: '16.x'
- run: npm install -g yarn
- uses: actions/checkout@v2
- name: Download Webview2
run: Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'MicrosoftEdgeWebview2Setup.exe'
if: runner.os == 'Windows'
shell: pwsh
- name: Build
run: cargo build --verbose

2
.gitignore vendored
View file

@ -5,3 +5,5 @@
**/*.rs.bk
*.log
*.exe

View file

@ -1,11 +0,0 @@
os: linux
dist: trusty
sudo: required
services:
- docker
install:
- docker pull ubuntu:18.04
script:
- docker run -v $(pwd):/liftinstall ubuntu:18.04 /bin/bash -ex /liftinstall/.travis/build.sh

View file

@ -1,10 +1,6 @@
#!/usr/bin/env bash
cd /liftinstall
cd /liftinstall || exit 1
apt update
apt install -y libwebkit2gtk-4.0-dev libssl-dev
yarn --cwd ui
curl https://sh.rustup.rs -sSf | sh -s -- -y
export PATH=~/.cargo/bin:$PATH
cargo build
cargo build --release

4
.travis/exec.sh Normal file
View file

@ -0,0 +1,4 @@
#!/bin/bash -ex
# the UID for the container yuzu user is 1027
docker run -u root -v $(pwd):/liftinstall -t yuzuemu/build-environments:linux-liftinstall /bin/bash /liftinstall/.travis/build.sh

8
.tx/config Executable file
View file

@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com
[o:yuzu-emulator:p:yuzu:r:installer]
file_filter = ui/translations/<lang>.po
source_file = ui/translations/en.po
source_lang = en
type = PO

4150
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,55 +1,84 @@
[package]
name = "liftinstall"
version = "0.1.0"
version = "0.2.0"
edition = "2018"
authors = ["James <jselby@jselby.net>"]
repository = "https://github.com/j-selby/liftinstall.git"
documentation = "https://liftinstall.jselby.net"
description = "An adaptable installer for your application."
build = "build.rs"
resolver = "2"
[dependencies]
web-view = {git = "https://github.com/Boscop/web-view.git", rev = "555f422d09cbb94e82a728d47e9e07ca91963f6e"}
anyhow = "^1"
wry = "0.12"
tinyfiledialogs = "3.8"
hyper = "0.11.27"
futures = "*"
mime_guess = "1.8.3"
url = "*"
futures = "0.1.29"
mime_guess = "2.0"
url = "2.2"
reqwest = "0.9.0"
number_prefix = "0.2.7"
reqwest = "0.9.22"
number_prefix = "0.4"
serde = "1.0.27"
serde_derive = "1.0.27"
serde_json = "1.0.9"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
toml = "0.4"
toml = "0.5"
semver = {version = "0.9.0", features = ["serde"]}
regex = "0.2"
semver = {version = "1.0", features = ["serde"]}
regex = "1.4"
dirs = "1.0"
zip = "0.4.2"
xz-decom = {git = "https://github.com/j-selby/xz-decom.git", rev = "9ebf3d00d9ff909c39eec1d2cf7e6e068ce214e5"}
dirs = "^4"
zip = "0.6"
xz2 = "0.1"
tar = "0.4"
log = "0.4"
fern = "0.5"
chrono = "0.4.5"
fern = "0.6"
chrono = "0.4"
clap = "2.32.0"
clap = "2.33"
# used to open a link to the users default browser
webbrowser = "0.8"
# used in JWT based package authentication
jsonwebtoken = "^8"
# used to decode the public key for verifying JWT tokens
base64 = "0.13"
[build-dependencies]
walkdir = "2"
serde = "1.0.27"
serde_derive = "1.0.27"
toml = "0.4"
walkdir = "2.3"
serde = "1.0"
serde_derive = "1.0"
toml = "0.5"
which = "4.0"
image = { version = "0.24", default-features = false, features = ["ico"] }
[target.'cfg(windows)'.dependencies]
# NFD is needed on Windows, as web-view doesn't work correctly here
nfd = "0.0.4"
winapi = { version = "0.3", features = ["psapi", "winbase", "winioctl", "winnt"] }
widestring = "^1"
webview2 = "0.1"
tempfile = "3"
[target.'cfg(not(windows))'.dependencies]
sysinfo = "0.26"
slug = "0.1"
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"
cc = "1.0"
[profile.release]
#panic = "abort"
lto = true
opt-level = "z"
codegen-units = 1
incremental = false
#[profile.release.overrides."*"] # +
#opt-level = "z"
#codegen-units = 1
#incremental = false

18
Justfile Normal file
View file

@ -0,0 +1,18 @@
ui-build:
yarn --cwd {{ justfile_directory() }}/ui/ install
yarn --cwd {{ justfile_directory() }}/ui/ build
ui-test:
cd {{ justfile_directory() }}/ui/ && node mock-server.js &
yarn --cwd {{ justfile_directory() }}/ui/ serve
update-i18n:
#!/bin/bash -e
[ -z "${TX_PULL}" ] || tx pull -a --minimum-perc 85
for i in {{ justfile_directory() }}/ui/translations/*.po; do
TARGET_FILE="$(basename $i)"
TARGET_LANG="${TARGET_FILE/.po/}"
OUTPUT="{{ justfile_directory() }}/ui/src/locales/${TARGET_LANG}.json"
i18next-conv -l en -s "$i" -t "$OUTPUT" -K
node {{ justfile_directory() }}/ui/unbreak-translations.js "$OUTPUT" "$OUTPUT"
done

View file

@ -22,6 +22,7 @@ For more detailed instructions, look at the usage documentation above.
There are are few system dependencies depending on your platform:
- For all platforms, `cargo` should be available on your PATH. [Rustup](https://rustup.rs/) is the
recommended way to achieve this. Stable or Nightly Rust works fine.
- Have node.js and Yarn available on your PATH (for building UI components, not needed at runtime).
- For Windows (MSVC), you need Visual Studio installed.
- For Windows (Mingw), you need `gcc`/`g++` available on the PATH.
- For Mac, you need Xcode installed, and Clang/etc available on the PATH.
@ -33,8 +34,8 @@ apt install -y build-essential libwebkit2gtk-4.0-dev libssl-dev
In order to build yourself an installer, as a bare minimum, you need to:
- Add your favicon to `static/favicon.ico`
- Add your logo to `static/logo.png`
- Add your favicon to `ui/public/favicon.ico`
- Add your logo to `ui/src/assets/logo.png`
- Modify the bootstrap configuration file as needed (`config.PLATFORM.toml`).
- Have the main configuration file somewhere useful, reachable over HTTP.
- Run:

13
SECURITY.md Normal file
View file

@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
As `liftinstall` is a template for your project, no specific versioning is
provided at this time, though a rough version is given in the Cargo file.
Only the latest version from this file will be supported.
## Reporting a Vulnerability
For any specific security concerns/vulnerabilities, please email me directly
at security *at* jselby.net.

View file

@ -1,2 +1,2 @@
name = "yuzu"
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.linux.v1.toml"
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.linux.v3.toml"

3
bootstrap.macos.toml Normal file
View file

@ -0,0 +1,3 @@
# fake configuration for CI purpose only
name = "yuzu"
target_url = "https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml"

View file

@ -1,2 +1,2 @@
name = "yuzu"
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v6.toml"
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v10.toml"

133
build.rs
View file

@ -1,5 +1,3 @@
extern crate walkdir;
#[cfg(windows)]
extern crate winres;
@ -11,23 +9,21 @@ extern crate serde;
extern crate serde_derive;
extern crate toml;
use walkdir::WalkDir;
extern crate which;
use std::env;
use std::io::Write;
use std::path::PathBuf;
use std::fs::copy;
use std::fs::create_dir_all;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::process::Command;
use std::env::consts::OS;
const FILES_TO_PREPROCESS: &'static [&'static str] = &["helpers.js", "views.js"];
use image::imageops::FilterType;
/// Describes the application itself.
#[derive(Debug, Deserialize)]
@ -39,7 +35,7 @@ pub struct BaseAttributes {
#[cfg(windows)]
fn handle_binary(config: &BaseAttributes) {
let mut res = winres::WindowsResource::new();
res.set_icon("static/favicon.ico");
res.set_icon("ui/public/favicon.ico");
res.set(
"FileDescription",
&format!("Interactive installer for {}", config.name),
@ -53,6 +49,8 @@ fn handle_binary(config: &BaseAttributes) {
cc::Build::new()
.cpp(true)
.define("_WIN32_WINNT", Some("0x0600"))
.define("WINVER", Some("0x0600"))
.file("src/native/interop.cpp")
.compile("interop");
}
@ -62,9 +60,18 @@ fn handle_binary(_config: &BaseAttributes) {}
fn main() {
let output_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let current_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let ui_dir = current_dir.join("ui");
let os = OS.to_lowercase();
#[cfg(windows)]
{
if std::fs::metadata("MicrosoftEdgeWebview2Setup.exe").is_err() {
panic!("Please download MicrosoftEdgeWebview2Setup.exe from https://go.microsoft.com/fwlink/p/?LinkId=2124703 and put the file at the workspace root!");
}
}
// Find target config
let target_config = PathBuf::from(format!("bootstrap.{}.toml", os));
@ -92,80 +99,42 @@ fn main() {
// Copy for the main build
copy(&target_config, output_dir.join("bootstrap.toml")).expect("Unable to copy config file");
// Copy files from static/ to build dir
for entry in WalkDir::new("static") {
let entry = entry.expect("Unable to read output directory");
let yarn_binary =
which::which("yarn").expect("Failed to find yarn - please go ahead and install it!");
let output_file = output_dir.join(entry.path());
// bundle the icon
let mut f = File::create(output_dir.join("icon-data.bin")).unwrap();
let icon_file = image::open("ui/public/favicon.ico").expect("Unable to read the icon file");
let icon_data = icon_file
.resize_exact(48, 48, FilterType::Triangle)
.to_rgba8();
f.write_all(&icon_data.into_vec()).unwrap();
if entry.path().is_dir() {
create_dir_all(output_file).expect("Unable to create dir");
} else {
let filename = entry
.path()
.file_name()
.expect("Unable to parse filename")
// Build and deploy frontend files
Command::new(&yarn_binary)
.arg("--version")
.spawn()
.expect("Yarn could not be launched");
Command::new(&yarn_binary)
.arg("--cwd")
.arg(ui_dir.to_str().expect("Unable to covert path"))
.spawn()
.unwrap()
.wait()
.expect("Unable to install Node.JS dependencies using Yarn");
let return_code = Command::new(&yarn_binary)
.args(&[
"--cwd",
ui_dir.to_str().expect("Unable to covert path"),
"run",
"build",
"--dest",
output_dir
.join("static")
.to_str()
.expect("Unable to convert to string");
if FILES_TO_PREPROCESS.contains(&filename) {
// Do basic preprocessing - transcribe template string
let source = BufReader::new(File::open(entry.path()).expect("Unable to copy file"));
let mut target = File::create(output_file).expect("Unable to copy file");
let mut is_template_string = false;
for line in source.lines() {
let line = line.expect("Unable to read line from JS file");
let mut is_break = false;
let mut is_quote = false;
let mut output_line = String::new();
if is_template_string {
output_line += "\"";
}
for c in line.chars() {
if c == '\\' {
is_break = true;
output_line.push('\\');
continue;
}
if (c == '\"' || c == '\'') && !is_break && !is_template_string {
is_quote = !is_quote;
}
if c == '`' && !is_break && !is_quote {
output_line += "\"";
is_template_string = !is_template_string;
continue;
}
if c == '"' && !is_break && is_template_string {
output_line += "\\\"";
continue;
}
is_break = false;
output_line.push(c);
}
if is_template_string {
output_line += "\" +";
}
output_line.push('\n');
target
.write(output_line.as_bytes())
.expect("Unable to write line");
}
} else {
copy(entry.path(), output_file).expect("Unable to copy file");
}
}
}
.expect("Unable to convert path"),
])
.status()
.expect("Unable to build frontend assets using Webpack");
assert!(return_code.success());
}

52
config.linux.v2.toml Normal file
View file

@ -0,0 +1,52 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
[authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
# "patreonInfo": { "linked": false, "activeSubscription": false }
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
[authentication.validation]
iss = "citra-core"
aud = "installer"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
default = true
[packages.source]
name = "github"
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-windows-msvc/yuzu.exe"
description = "Launch yuzu"
[[packages]]
name = "yuzu Early Access"
description = "Bonus preview release for project supporters. Thanks for your support!"
# Displayed when the package has no authentication for the user
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
# Displayed when the package has an authentication, but the user has not linked their account
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
requires_authorization = true
# puts a "new" ribbon the package select
is_new = true
[packages.source]
name = "patreon"
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "earlyaccess"
[[packages.shortcuts]]
name = "yuzu Early Access"
relative_path = "yuzu-linux-earlyaccess/yuzu.exe"
description = "Launch yuzu Early Access"

58
config.linux.v3.toml Normal file
View file

@ -0,0 +1,58 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
[authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
# "patreonInfo": { "linked": false, "activeSubscription": false }
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
[authentication.validation]
iss = "citra-core"
aud = "installer"
[[packages]]
name = "yuzu Early Access"
description = "Preview release with the newest features for the supporters."
icon = "thicc_logo_installer__ea_shadow.png"
requires_authorization = true
# puts a "new" ribbon the package select
is_new = true
[packages.extended_description]
no_action_description = "Thank you for your support!"
# Displayed when the package has no authentication for the user
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
# Displayed when the package has an authentication, but the user has not linked their account
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
[packages.source]
name = "patreon"
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "earlyaccess"
[[packages.shortcuts]]
name = "yuzu Early Access"
relative_path = "yuzu-linux-early-access/yuzu-early-access.AppImage"
description = "Launch yuzu Early Access"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
icon = "thicc_logo_installer_shadow.png"
default = true
[packages.source]
name = "github"
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-linux-mainline/yuzu-mainline.AppImage"
description = "Launch yuzu"

58
config.windows.v10.toml Normal file
View file

@ -0,0 +1,58 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
[authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
# "patreonInfo": { "linked": false, "activeSubscription": false }
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
[authentication.validation]
iss = "citra-core"
aud = "installer"
[[packages]]
name = "yuzu Early Access"
description = "Preview release with the newest features for the supporters."
icon = "thicc_logo_installer__ea_shadow.png"
requires_authorization = true
# puts a "new" ribbon the package select
is_new = true
[packages.extended_description]
no_action_description = "Thank you for your support!"
# Displayed when the package has no authentication for the user
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
# Displayed when the package has an authentication, but the user has not linked their account
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
[packages.source]
name = "patreon"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "earlyaccess"
[[packages.shortcuts]]
name = "yuzu Early Access"
relative_path = "yuzu-windows-msvc-early-access/yuzu.exe"
description = "Launch yuzu Early Access"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
icon = "thicc_logo_installer_shadow.png"
default = true
[packages.source]
name = "github"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-windows-msvc/yuzu.exe"
description = "Launch yuzu"

View file

@ -1,5 +1,6 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.5/yuzu_install.exe"
[[packages]]
name = "yuzu Nightly"

31
config.windows.v7.toml Normal file
View file

@ -0,0 +1,31 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.6/yuzu_install.exe"
[[packages]]
name = "yuzu Nightly"
description = "The nightly build of yuzu contains already reviewed and tested features."
[packages.source]
name = "github"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$"
[packages.source.config]
repo = "yuzu-emu/yuzu-nightly"
[[packages.shortcuts]]
name = "yuzu Nightly"
relative_path = "nightly/yuzu.exe"
description = "Launch yuzu (Nightly version)"
[[packages]]
name = "yuzu Canary"
description = "The canary build of yuzu has additional features that are still waiting on review."
default = true
[packages.source]
name = "github"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.zip$"
[packages.source.config]
repo = "yuzu-emu/yuzu-canary"
[[packages.shortcuts]]
name = "yuzu Canary"
relative_path = "canary/yuzu.exe"
description = "Launch yuzu (Canary version)"

17
config.windows.v8.toml Normal file
View file

@ -0,0 +1,17 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.7/yuzu_install.exe"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
default = true
[packages.source]
name = "github"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-windows-msvc/yuzu.exe"
description = "Launch yuzu"

52
config.windows.v9.toml Normal file
View file

@ -0,0 +1,52 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
[authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
# "patreonInfo": { "linked": false, "activeSubscription": false }
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
[authentication.validation]
iss = "citra-core"
aud = "installer"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
default = true
[packages.source]
name = "github"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-windows-msvc/yuzu.exe"
description = "Launch yuzu"
[[packages]]
name = "yuzu Early Access"
description = "Bonus preview release for project supporters. Thanks for your support!"
# Displayed when the package has no authentication for the user
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
# Displayed when the package has an authentication, but the user has not linked their account
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
requires_authorization = true
# puts a "new" ribbon the package select
is_new = true
[packages.source]
name = "patreon"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "earlyaccess"
[[packages.shortcuts]]
name = "yuzu Early Access"
relative_path = "yuzu-windows-msvc-early-access/yuzu.exe"
description = "Launch yuzu Early Access"

View file

@ -10,13 +10,13 @@ use std::io::Read;
use std::iter::Iterator;
use std::path::PathBuf;
use xz_decom;
use xz2::read::XzDecoder;
pub trait Archive<'a> {
/// func: iterator value, max size, file name, file contents
fn for_each(
&mut self,
func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
func: &mut dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>,
) -> Result<(), String>;
}
@ -27,7 +27,7 @@ struct ZipArchive<'a> {
impl<'a> Archive<'a> for ZipArchive<'a> {
fn for_each(
&mut self,
func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
func: &mut dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>,
) -> Result<(), String> {
let max = self.archive.len();
@ -41,7 +41,7 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
continue;
}
func(i, Some(max), archive.sanitized_name(), &mut archive)?;
func(i, Some(max), archive.mangled_name(), &mut archive)?;
}
Ok(())
@ -49,13 +49,13 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
}
struct TarArchive<'a> {
archive: UpstreamTarArchive<Box<Read + 'a>>,
archive: UpstreamTarArchive<Box<dyn Read + 'a>>,
}
impl<'a> Archive<'a> for TarArchive<'a> {
fn for_each(
&mut self,
func: &mut FnMut(usize, Option<usize>, PathBuf, &mut Read) -> Result<(), String>,
func: &mut dyn FnMut(usize, Option<usize>, PathBuf, &mut dyn Read) -> Result<(), String>,
) -> Result<(), String> {
let entries = self
.archive
@ -83,7 +83,7 @@ impl<'a> Archive<'a> for TarArchive<'a> {
}
/// Reads the named archive with an archive implementation.
pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<Archive<'a> + 'a>, String> {
pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<dyn Archive<'a> + 'a>, String> {
if name.ends_with(".zip") {
// Decompress a .zip file
let archive = UpstreamZipArchive::new(Cursor::new(data))
@ -92,10 +92,13 @@ pub fn read_archive<'a>(name: &str, data: &'a [u8]) -> Result<Box<Archive<'a> +
Ok(Box::new(ZipArchive { archive }))
} else if name.ends_with(".tar.xz") {
// Decompress a .tar.xz file
let decompressed_data = xz_decom::decompress(data)
.map_err(|x| format!("Failed to build decompressor: {:?}", x))?;
let mut decompresser = XzDecoder::new(data);
let mut decompressed_data = Vec::new();
decompresser
.read_to_end(&mut decompressed_data)
.map_err(|x| format!("Failed to decompress data: {:?}", x))?;
let decompressed_contents: Box<Read> = Box::new(Cursor::new(decompressed_data));
let decompressed_contents: Box<dyn Read> = Box::new(Cursor::new(decompressed_data));
let tar = UpstreamTarArchive::new(decompressed_contents);

View file

@ -7,8 +7,8 @@ use toml::de::Error as TomlError;
use serde_json::{self, Error as SerdeError};
use sources::get_by_name;
use sources::types::Release;
use crate::sources::get_by_name;
use crate::sources::types::Release;
/// Description of the source of a package.
#[derive(Debug, Deserialize, Serialize, Clone)]
@ -25,6 +25,23 @@ pub struct PackageShortcut {
pub name: String,
pub relative_path: String,
pub description: String,
#[serde(default)]
pub has_desktop_shortcut: bool,
}
/// Extra description for authentication and authorization state for a package
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PackageExtendedDescription {
#[serde(default)]
pub no_action_description: Option<String>,
#[serde(default)]
pub need_authentication_description: Option<String>,
#[serde(default)]
pub need_link_description: Option<String>,
#[serde(default)]
pub need_subscription_description: Option<String>,
#[serde(default)]
pub need_reward_tier_description: Option<String>,
}
/// Describes a overview of a individual package.
@ -32,10 +49,34 @@ pub struct PackageShortcut {
pub struct PackageDescription {
pub name: String,
pub description: String,
#[serde(default)]
pub icon: Option<String>,
pub default: Option<bool>,
pub source: PackageSource,
#[serde(default)]
pub shortcuts: Vec<PackageShortcut>,
#[serde(default)]
pub requires_authorization: Option<bool>,
#[serde(default)]
pub is_new: Option<bool>,
#[serde(default)]
pub extended_description: Option<PackageExtendedDescription>,
}
/// Configuration for validating the JWT token
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JWTValidation {
pub iss: Option<String>,
// This can technically be a Vec as well, but thats a pain to support atm
pub aud: Option<String>,
}
/// The configuration for this release.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AuthenticationConfig {
pub pub_key_base64: String,
pub auth_url: String,
pub validation: Option<JWTValidation>,
}
/// Describes the application itself.
@ -43,6 +84,8 @@ pub struct PackageDescription {
pub struct BaseAttributes {
pub name: String,
pub target_url: String,
#[serde(default)]
pub recovery: bool,
}
impl BaseAttributes {
@ -66,6 +109,8 @@ pub struct Config {
pub packages: Vec<PackageDescription>,
#[serde(default)]
pub hide_advanced: bool,
#[serde(default)]
pub authentication: Option<AuthenticationConfig>,
}
impl Config {

29
src/frontend/mod.rs Normal file
View file

@ -0,0 +1,29 @@
//! frontend/mod.rs
//!
//! Provides the frontend interface, including HTTP server.
use std::sync::{Arc, RwLock};
use crate::installer::InstallerFramework;
use crate::logging::LoggingErrors;
pub mod rest;
mod ui;
/// Launches the main web server + UI. Returns when the framework has been consumed + web UI closed.
pub fn launch(app_name: &str, is_launcher: bool, framework: InstallerFramework) {
let framework = Arc::new(RwLock::new(framework));
let (servers, address) = rest::server::spawn_servers(framework.clone());
ui::start_ui(app_name, &address, is_launcher).log_expect("Failed to start UI");
// Explicitly hint that we want the servers instance until here.
drop(servers);
framework
.write()
.log_expect("Failed to write to framework to finalize")
.shutdown()
.log_expect("Failed to finalize framework");
}

View file

@ -2,7 +2,8 @@
extern crate mime_guess;
use assets::mime_guess::{get_mime_type, octet_stream};
use self::mime_guess::from_ext;
use self::mime_guess::mime::APPLICATION_OCTET_STREAM;
macro_rules! include_files_as_assets {
( $target_match:expr, $( $file_name:expr ),* ) => {
@ -23,9 +24,9 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
Some(ext_ptr) => {
let ext = &file_path[ext_ptr + 1..];
get_mime_type(ext)
from_ext(ext).first_or_octet_stream()
}
None => octet_stream(),
None => APPLICATION_OCTET_STREAM,
};
let string_mime = guessed_mime.to_string();
@ -34,18 +35,21 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
file_path,
"/index.html",
"/favicon.ico",
"/logo.png",
"/how-to-open.png",
"/css/bulma.min.css",
"/css/main.css",
"/img/light_mode_installer_logo.png",
"/img/dark_mode_installer_logo.png",
"/thicc_logo_installer__ea_shadow.png",
"/thicc_logo_installer_shadow.png",
"/img/how-to-open.png",
"/css/app.css",
"/css/chunk-vendors.css",
"/fonts/roboto-v18-latin-regular.eot",
"/fonts/roboto-v18-latin-regular.woff",
"/fonts/roboto-v18-latin-regular.woff2",
"/js/vue.min.js",
"/js/vue-router.min.js",
"/js/helpers.js",
"/js/views.js",
"/js/main.js"
"/fonts/materialdesignicons-webfont.eot",
"/fonts/materialdesignicons-webfont.woff",
"/fonts/materialdesignicons-webfont.woff2",
"/js/chunk-vendors.js",
"/js/app.js"
)?;
Some((string_mime, contents))

7
src/frontend/rest/mod.rs Normal file
View file

@ -0,0 +1,7 @@
//! frontend/rest/mod.rs
//!
//! Contains the main web server used within the application.
mod assets;
pub mod server;
pub mod services;

View file

@ -0,0 +1,86 @@
//! frontend/rest/server.rs
//!
//! Contains the over-arching server object + methods to manipulate it.
use crate::frontend::rest::services::WebService;
use crate::installer::InstallerFramework;
use crate::logging::LoggingErrors;
use hyper::server::Http;
use std::sync::{Arc, RwLock};
use std::net::{SocketAddr, TcpListener, ToSocketAddrs};
use std::thread;
use std::thread::JoinHandle;
/// Acts as a communication mechanism between the Hyper WebService and the rest of the
/// application.
pub struct WebServer {
_handle: JoinHandle<()>,
}
impl WebServer {
/// Creates a new web server with the specified address.
pub fn with_addr(
framework: Arc<RwLock<InstallerFramework>>,
addr: SocketAddr,
) -> Result<Self, hyper::Error> {
let handle = thread::spawn(move || {
let server = Http::new()
.bind(&addr, move || Ok(WebService::new(framework.clone())))
.log_expect("Failed to bind to port");
server.run().log_expect("Failed to run HTTP server");
});
Ok(WebServer { _handle: handle })
}
}
/// Spawns a server instance on all local interfaces.
///
/// Returns server instances + http address of service running.
pub fn spawn_servers(framework: Arc<RwLock<InstallerFramework>>) -> (Vec<WebServer>, String) {
// Firstly, allocate us an epidermal port
let target_port = {
let listener = TcpListener::bind("127.0.0.1:0")
.log_expect("At least one local address should be free");
listener
.local_addr()
.log_expect("Should be able to pull address from listener")
.port()
};
// Now, iterate over all ports
let addresses = "localhost:0"
.to_socket_addrs()
.log_expect("No localhost address found");
let mut instances = Vec::with_capacity(addresses.len());
let mut http_address = None;
// Startup HTTP server for handling the web view
for mut address in addresses {
address.set_port(target_port);
let server = WebServer::with_addr(framework.clone(), address)
.log_expect("Failed to bind to address");
info!("Spawning server instance @ {:?}", address);
http_address = Some(address);
instances.push(server);
}
let http_address = http_address.log_expect("No HTTP address found");
(
instances,
format!("http://localhost:{}", http_address.port()),
)
}

View file

@ -0,0 +1,29 @@
//! frontend/rest/services/attributes.rs
//!
//! The /api/attr call returns an executable script containing session variables.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.get_framework_read();
let file = framework
.base_attributes
.to_json_str()
.log_expect("Failed to render JSON representation of config");
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -0,0 +1,293 @@
//! frontend/rest/services/authentication.rs
//!
//! Provides mechanisms to authenticate users using JWT.
use std::collections::HashSet;
use std::sync::Arc;
use futures::{Future, Stream};
use hyper::header::{ContentLength, ContentType};
use jsonwebtoken::DecodingKey;
use jwt::{decode, Algorithm, Validation};
use reqwest::header::USER_AGENT;
use crate::frontend::rest::services::Future as InternalFuture;
use crate::frontend::rest::services::{default_future, Request, Response, WebService};
use crate::http::{build_async_client, build_client};
use crate::config::JWTValidation;
use crate::logging::LoggingErrors;
#[derive(Debug, Serialize, Deserialize)]
struct Auth {
username: String,
token: String,
jwt_token: Option<JWTClaims>,
}
/// claims struct, it needs to derive `Serialize` and/or `Deserialize`
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JWTClaims {
pub sub: String,
pub iss: String,
pub aud: String,
pub exp: usize,
#[serde(default)]
pub roles: Vec<String>,
#[serde(rename = "releaseChannels", default)]
pub channels: Vec<String>,
#[serde(rename = "isPatreonAccountLinked")]
pub is_linked: bool,
#[serde(rename = "isPatreonSubscriptionActive")]
pub is_subscribed: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct AuthRequest {
username: String,
token: String,
}
/// Calls the given server to obtain a JWT token and returns a Future<String> with the response
pub fn authenticate_async(
url: String,
username: String,
token: String,
) -> Box<dyn futures::Future<Item = String, Error = String>> {
// Build the HTTP client up
let client = match build_async_client() {
Ok(v) => v,
Err(_) => {
return Box::new(futures::future::err(
"Unable to build async web client".to_string(),
));
}
};
Box::new(client.post(&url)
.header(USER_AGENT, "liftinstall (j-selby)")
.header("X-USERNAME", username.clone())
.header("X-TOKEN", token.clone())
.send()
.map_err(|err| {
format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}",
err, err.is_client_error(), err.is_http(), err.is_redirect(),
err.is_serialization(), err.is_timeout(), err.is_server_error())
})
.map(|mut response| {
match response.status() {
reqwest::StatusCode::OK =>
Ok(response.text()
.map_err(|e| {
format!("Error while converting the response to text {:?}", e)
})),
_ => {
Err(format!("Error wrong response code from server {:?}", response.status()))
}
}
})
.and_then(|x| x)
.flatten()
)
}
pub fn authenticate_sync(url: String, username: String, token: String) -> Result<String, String> {
// Build the HTTP client up
let client = build_client()?;
let mut response = client.post(&url)
.header(USER_AGENT, "liftinstall (j-selby)")
.header("X-USERNAME", username.clone())
.header("X-TOKEN", token.clone())
.send()
.map_err(|err| {
format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}",
err, err.is_client_error(), err.is_http(), err.is_redirect(),
err.is_serialization(), err.is_timeout(), err.is_server_error())
})?;
match response.status() {
reqwest::StatusCode::OK => Ok(response
.text()
.map_err(|e| format!("Error while converting the response to text {:?}", e))?),
_ => Err(format!(
"Error wrong response code from server {:?}",
response.status()
)),
}
}
pub fn validate_token(
body: String,
pub_key_base64: String,
validation: Option<JWTValidation>,
) -> Result<JWTClaims, String> {
// Get the public key for this authentication url
let pub_key = if pub_key_base64.is_empty() {
vec![]
} else {
base64::decode(&pub_key_base64).map_err(|e| {
format!(
"Configured public key was not empty and did not decode as base64 {:?}",
e
)
})?
};
// Configure validation for audience and issuer if the configuration provides it
let mut validation = match validation {
Some(v) => {
let mut valid = Validation::new(Algorithm::RS256);
valid.iss = v.iss.map(|iss| {
let mut issuer = HashSet::new();
issuer.insert(iss);
issuer
});
if let &Some(ref v) = &v.aud {
valid.set_audience(&[v]);
}
valid
}
None => Validation::default(),
};
validation.validate_exp = false;
validation.validate_nbf = false;
// Verify the JWT token
decode::<JWTClaims>(&body, &DecodingKey::from_rsa_der(&pub_key), &validation)
.map(|tok| tok.claims)
.map_err(|err| {
format!(
"Error while decoding the JWT. error: {:?} jwt: {:?}",
err, body
)
})
}
pub fn handle(service: &WebService, _req: Request) -> InternalFuture {
info!("Handling authentication");
let framework = service
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let credentials = framework.database.credentials.clone();
let config = framework
.config
.clone()
.log_expect("No in-memory configuration found");
// If authentication isn't configured, just return immediately
if config.authentication.is_none() {
return default_future(Response::new().with_status(hyper::Ok).with_body("{}"));
}
// Create moveable framework references so that the lambdas can write to them later
let write_cred_fw = Arc::clone(&service.framework);
Box::new(
_req.body()
.concat2()
.map(move |body| {
let req = serde_json::from_slice::<AuthRequest>(&body);
if req.is_err() {
warn!("Failed to parse auth request from the frontend");
return default_future(
Response::new().with_status(hyper::StatusCode::BadRequest),
);
}
let req = req.unwrap();
// Determine which credentials we should use
let (username, token) = {
let req_username = req.username;
let req_token = req.token;
// if the user didn't provide credentials, and theres nothing stored in the
// database, return an early error
let req_cred_valid = !req_username.is_empty() && !req_token.is_empty();
let stored_cred_valid =
!credentials.username.is_empty() && !credentials.token.is_empty();
if !req_cred_valid && !stored_cred_valid {
info!("No passed in credential and no stored credentials to validate");
return default_future(Response::new().with_status(hyper::BadRequest));
}
if req_cred_valid {
(req_username.clone(), req_token.clone())
} else {
(credentials.username.clone(), credentials.token.clone())
}
};
// second copy of the credentials so we can move them into a different closure
let (username_clone, token_clone) = (username.clone(), token.clone());
let authentication = config
.authentication
.log_expect("No authentication configuration");
let auth_url = authentication.auth_url.clone();
let pub_key_base64 = authentication.pub_key_base64.clone();
let validation = authentication.validation.clone();
// call the authentication URL to see if we are authenticated
Box::new(
authenticate_async(auth_url, username.clone(), token.clone())
.map(|body| validate_token(body, pub_key_base64, validation))
.and_then(|res| res)
.map(move |claims| {
let out = Auth {
username: username_clone,
token: token_clone,
jwt_token: Some(claims.clone()),
};
// Convert the json to a string and return the json token
match serde_json::to_string(&out) {
Ok(v) => Ok(v),
Err(e) => Err(format!(
"Error while converting the claims to JSON string: {:?}",
e
)),
}
})
.and_then(|res| res)
.map(move |json| {
{
// Store the validated username and password into the installer database
let mut framework = write_cred_fw
.write()
.log_expect("InstallerFramework has been dirtied");
framework.database.credentials.username = username;
framework.database.credentials.token = token;
}
// Finally return the JSON with the response
info!("successfully verified username and token");
Response::new()
.with_header(ContentLength(json.len() as u64))
.with_header(ContentType::json())
.with_status(hyper::StatusCode::Ok)
.with_body(json)
})
.map_err(|err| {
error!(
"Got an internal error while processing user token: {:?}",
err
);
Response::new().with_status(hyper::StatusCode::InternalServerError)
})
.or_else(|err| {
// Convert the Err value into an Ok value since the error code from
// this HTTP request is an Ok(response)
Ok(err)
}),
)
})
// Flatten the internal future into the output response future
.flatten(),
)
}

View file

@ -0,0 +1,31 @@
//! frontend/rest/services/browser.rs
//!
//! Launches the user's web browser on request from the frontend.
use crate::frontend::rest::services::Future as InternalFuture;
use crate::frontend::rest::services::{Request, Response, WebService};
use crate::logging::LoggingErrors;
use futures::{Future, Stream};
use hyper::header::ContentType;
#[derive(Debug, Serialize, Deserialize, Clone)]
struct OpenRequest {
url: String,
}
pub fn handle(_service: &WebService, req: Request) -> InternalFuture {
Box::new(req.body().concat2().map(move |body| {
let req: OpenRequest = serde_json::from_slice(&body).log_expect("Malformed request");
if webbrowser::open(&req.url).is_ok() {
Response::new()
.with_status(hyper::Ok)
.with_header(ContentType::json())
.with_body("{}")
} else {
Response::new()
.with_status(hyper::BadRequest)
.with_header(ContentType::json())
.with_body("{}")
}
}))
}

View file

@ -0,0 +1,84 @@
//! frontend/rest/services/config.rs
//!
//! The /api/config call returns the current installer framework configuration.
//!
//! This endpoint should be usable directly from a <script> tag during loading.
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
use crate::config::Config;
use crate::http::build_async_client;
use futures::stream::Stream;
use futures::Future as _;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework_url = {
service
.get_framework_read()
.base_attributes
.target_url
.clone()
};
info!("Downloading configuration from {:?}...", framework_url);
let framework = service.framework.clone();
// Hyper doesn't allow for clients to do sync network operations in a async future.
// This smallish pipeline joins the two together.
Box::new(
build_async_client()
.log_expect("Failed to build async client")
.get(&framework_url)
.send()
.map_err(|x| {
error!("HTTP error while downloading configuration file: {:?}", x);
hyper::Error::Incomplete
})
.and_then(|x| {
x.into_body().concat2().map_err(|x| {
error!("HTTP error while parsing configuration file: {:?}", x);
hyper::Error::Incomplete
})
})
.and_then(move |x| {
let x = String::from_utf8(x.to_vec()).map_err(|x| {
error!("UTF-8 error while parsing configuration file: {:?}", x);
hyper::Error::Incomplete
})?;
let config = Config::from_toml_str(&x).map_err(|x| {
error!("Serde error while parsing configuration file: {:?}", x);
hyper::Error::Incomplete
})?;
let mut framework = framework
.write()
.log_expect("Failed to get write lock for framework");
framework.config = Some(config);
info!("Configuration file downloaded successfully.");
let file = framework
.get_config()
.log_expect("Config should be loaded by now")
.to_json_str()
.log_expect("Failed to render JSON representation of config");
Ok(Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file))
}),
)
}

View file

@ -0,0 +1,27 @@
//! frontend/rest/services/dark_mode.rs
//!
//! This call returns if dark mode is enabled on the system currently.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
use crate::native::is_dark_mode_active;
pub fn handle(_service: &WebService, _req: Request) -> Future {
let file = serde_json::to_string(&is_dark_mode_active())
.log_expect("Failed to render JSON payload of installation status object");
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -0,0 +1,35 @@
//! frontend/rest/services/default_path.rs
//!
//! The /api/default-path returns the default path for the application to install into.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
/// Struct used by serde to send a JSON payload to the client containing an optional value.
#[derive(Serialize)]
struct FileSelection {
path: Option<String>,
}
pub fn handle(service: &WebService, _req: Request) -> Future {
let path = { service.get_framework_read().get_default_path() };
let response = FileSelection { path };
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of default path object");
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -0,0 +1,32 @@
//! frontend/rest/services/exit.rs
//!
//! The /api/exit closes down the application.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::ContentType;
use hyper::StatusCode;
use std::process::exit;
pub fn handle(service: &WebService, _req: Request) -> Future {
match service.get_framework_write().shutdown() {
Ok(_) => {
exit(0);
}
Err(e) => {
error!("Failed to complete framework shutdown: {:?}", e);
default_future(
Response::new()
.with_status(StatusCode::InternalServerError)
.with_header(ContentType::plaintext())
.with_body(format!("Failed to complete framework shutdown - {}", e)),
)
}
}
}

View file

@ -0,0 +1,100 @@
//! frontend/rest/services/install.rs
//!
//! The /api/install call installs a set of packages dictated by a POST request.
use crate::frontend::rest::services::stream_progress;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::WebService;
use crate::logging::LoggingErrors;
use crate::installer::InstallMessage;
use futures::future::Future as _;
use futures::stream::Stream;
use url::form_urlencoded;
use std::collections::HashMap;
pub fn handle(service: &WebService, req: Request) -> Future {
let framework = service.framework.clone();
Box::new(req.body().concat2().map(move |b| {
let results = form_urlencoded::parse(b.as_ref())
.into_owned()
.collect::<HashMap<String, String>>();
let mut to_install = Vec::new();
let mut path: Option<String> = None;
let mut force_install = false;
let mut install_desktop_shortcut = false;
// Transform results into just an array of stuff to install
for (key, value) in &results {
if key == "path" {
path = Some(value.to_owned());
continue;
} else if key == "installDesktopShortcut" {
info!("Found installDesktopShortcut {:?}", value);
install_desktop_shortcut = value == "true";
continue;
}
if key == "mode" && value == "force" {
force_install = true;
continue;
}
if value == "true" {
to_install.push(key.to_owned());
}
}
if !install_desktop_shortcut {
let framework_ref = framework
.read()
.log_expect("InstallerFramework has been dirtied");
install_desktop_shortcut = framework_ref.preexisting_install
&& framework_ref
.database
.packages
.first()
.and_then(|x| Some(x.shortcuts.len() > 1))
.unwrap_or(false);
}
// The frontend always provides this
let path =
path.log_expect("No path specified by frontend when one should have already existed");
stream_progress(move |sender| {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
let new_install = !framework.preexisting_install;
if new_install {
framework.set_install_dir(&path);
}
if let Err(v) = framework.install(
to_install,
&sender,
new_install,
install_desktop_shortcut,
force_install,
) {
error!("Install error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send install error: {:?}", v);
}
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
})
}))
}

View file

@ -0,0 +1,32 @@
//! frontend/rest/services/installation_status.rs
//!
//! The /api/installation-status call returns metadata relating to the current status of
//! the installation.
//!
//! e.g. if the application is in maintenance mode
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.get_framework_read();
let response = framework.get_installation_status();
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of installation status object");
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -0,0 +1,158 @@
//! frontend/rest/services/mod.rs
//!
//! Provides all services used by the REST server.
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::installer::{InstallMessage, InstallerFramework};
use hyper::server::Service;
use hyper::{Method, StatusCode};
use crate::logging::LoggingErrors;
use std::sync::mpsc::{channel, Sender};
use std::thread;
use hyper::header::ContentType;
use futures::future::Future as _;
use futures::sink::Sink;
mod attributes;
pub mod authentication;
mod browser;
mod config;
mod dark_mode;
mod default_path;
mod exit;
mod install;
mod installation_status;
mod packages;
mod static_files;
mod uninstall;
mod update_updater;
mod verify_path;
mod view_folder;
/// Expected incoming Request format from Hyper.
pub type Request = hyper::server::Request;
/// Completed response type returned by the server.
pub type Response = hyper::server::Response;
/// Error type returned by the server.
pub type Error = hyper::Error;
/// The return type used by function calls to the web server.
pub type Future = Box<dyn futures::Future<Item = Response, Error = Error>>;
/// If advanced functionality is not needed, return a default instant future.
pub fn default_future(response: Response) -> Future {
Box::new(futures::future::ok(response))
}
/// Encapsulates JSON as a injectable Javascript script.
pub fn encapsulate_json(field_name: &str, json: &str) -> String {
format!("var {} = {};", field_name, json)
}
/// Streams messages from a specified task to the client in a thread.
pub fn stream_progress<F: 'static>(function: F) -> Response
where
F: FnOnce(Sender<InstallMessage>) -> () + Send,
{
let (sender, receiver) = channel();
let (tx, rx) = hyper::Body::pair();
// Startup a thread to do this operation for us
thread::spawn(move || function(sender));
// Spawn a thread for transforming messages to chunk messages
thread::spawn(move || {
let mut tx = tx;
loop {
let response = receiver
.recv()
.log_expect("Failed to receive message from runner thread");
if let InstallMessage::EOF = response {
break;
}
let mut response = serde_json::to_string(&response)
.log_expect("Failed to render JSON logging response payload");
response.push('\n');
tx = tx
.send(Ok(response.into_bytes().into()))
.wait()
.log_expect("Failed to write JSON response payload to client");
}
});
Response::new()
.with_header(ContentType::plaintext())
.with_body(rx)
}
/// Holds internal state for a single Hyper instance. Multiple will exist.
pub struct WebService {
framework: Arc<RwLock<InstallerFramework>>,
}
impl WebService {
/// Returns an immutable reference to the framework. May block.
pub fn get_framework_read(&self) -> RwLockReadGuard<InstallerFramework> {
self.framework
.read()
.log_expect("InstallerFramework has been dirtied")
}
/// Returns an immutable reference to the framework. May block.
pub fn get_framework_write(&self) -> RwLockWriteGuard<InstallerFramework> {
self.framework
.write()
.log_expect("InstallerFramework has been dirtied")
}
/// Creates a new WebService instance. Multiple are likely going to exist at once,
/// so create a lock to hold this.
pub fn new(framework: Arc<RwLock<InstallerFramework>>) -> WebService {
WebService { framework }
}
}
impl Service for WebService {
type Request = Request;
type Response = Response;
type Error = Error;
type Future = Future;
fn call(&self, req: Self::Request) -> Self::Future {
let method = req.method().clone();
let path = req.path().to_string();
match (method, path.as_str()) {
(Method::Get, "/api/attrs") => attributes::handle(self, req),
(Method::Get, "/api/config") => config::handle(self, req),
(Method::Get, "/api/dark-mode") => dark_mode::handle(self, req),
(Method::Get, "/api/default-path") => default_path::handle(self, req),
(Method::Get, "/api/exit") => exit::handle(self, req),
(Method::Get, "/api/packages") => packages::handle(self, req),
(Method::Get, "/api/installation-status") => installation_status::handle(self, req),
(Method::Get, "/api/view-local-folder") => view_folder::handle(self, req),
(Method::Post, "/api/check-auth") => authentication::handle(self, req),
(Method::Post, "/api/start-install") => install::handle(self, req),
(Method::Post, "/api/open-browser") => browser::handle(self, req),
(Method::Post, "/api/uninstall") => uninstall::handle(self, req),
(Method::Post, "/api/update-updater") => update_updater::handle(self, req),
(Method::Post, "/api/verify-path") => verify_path::handle(self, req),
(Method::Get, _) => static_files::handle(self, req),
e => {
info!("Returned 404 for {:?}", e);
default_future(Response::new().with_status(StatusCode::NotFound))
}
}
}
}

View file

@ -0,0 +1,31 @@
//! frontend/rest/services/packages.rs
//!
//! The /api/packages call returns all the currently installed packages.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::encapsulate_json;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.get_framework_read();
let file = encapsulate_json(
"packages",
&serde_json::to_string(&framework.database)
.log_expect("Failed to render JSON representation of database"),
);
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

View file

@ -0,0 +1,42 @@
//! frontend/rest/services/static_files.rs
//!
//! The static files call returns static files embedded within the executable.
//!
//! e.g. index.html, main.js, ...
use crate::frontend::rest::assets;
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use hyper::StatusCode;
use crate::logging::LoggingErrors;
pub fn handle(_service: &WebService, req: Request) -> Future {
// At this point, we have a web browser client. Search for a index page
// if needed
let mut path: String = req.path().to_owned();
if path.ends_with('/') {
path += "index.html";
}
default_future(match assets::file_from_string(&path) {
Some((content_type, file)) => {
let content_type = ContentType(
content_type
.parse()
.log_expect("Failed to parse content type into correct representation"),
);
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(content_type)
.with_body(file)
}
None => Response::new().with_status(StatusCode::NotFound),
})
}

View file

@ -0,0 +1,34 @@
//! frontend/rest/services/uninstall.rs
//!
//! The /api/uninstall call uninstalls all packages.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::stream_progress;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::WebService;
use crate::logging::LoggingErrors;
use crate::installer::InstallMessage;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.framework.clone();
default_future(stream_progress(move |sender| {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
if let Err(v) = framework.uninstall(&sender) {
error!("Uninstall error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send uninstall error: {:?}", v);
};
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
}))
}

View file

@ -0,0 +1,34 @@
//! frontend/rest/services/update_updater.rs
//!
//! The /api/update-updater call attempts to update the currently running updater.
use crate::frontend::rest::services::default_future;
use crate::frontend::rest::services::stream_progress;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::WebService;
use crate::logging::LoggingErrors;
use crate::installer::InstallMessage;
pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.framework.clone();
default_future(stream_progress(move |sender| {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
if let Err(v) = framework.update_updater(&sender) {
error!("Self-update error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send self-update error: {:?}", v);
};
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
}))
}

View file

@ -0,0 +1,48 @@
//! frontend/rest/services/verify_path.rs
//!
//! The /api/verify-path returns whether the path exists or not.
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use url::form_urlencoded;
use hyper::header::{ContentLength, ContentType};
use futures::future::Future as _;
use futures::stream::Stream;
use crate::logging::LoggingErrors;
use std::collections::HashMap;
use std::path::PathBuf;
/// Struct used by serde to send a JSON payload to the client containing an optional value.
#[derive(Serialize)]
struct VerifyResponse {
exists: bool,
}
pub fn handle(_service: &WebService, req: Request) -> Future {
Box::new(req.body().concat2().map(move |b| {
let results = form_urlencoded::parse(b.as_ref())
.into_owned()
.collect::<HashMap<String, String>>();
let mut exists = false;
if let Some(path) = results.get("path") {
let path = PathBuf::from(path);
exists = path.is_dir();
}
let response = VerifyResponse { exists };
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of default path object");
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}))
}

View file

@ -0,0 +1,35 @@
//! frontend/rest/services/view_folder.rs
//!
//! The /api/view-local-folder returns whether the path exists or not.
//! Side-effect: will open the folder in the default file manager if it exists.
use super::default_future;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::Response;
use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType};
use crate::logging::LoggingErrors;
use crate::native::open_in_shell;
pub fn handle(service: &WebService, _: Request) -> Future {
let framework = service.get_framework_read();
let mut response = false;
let path = framework.install_path.clone();
if let Some(path) = path {
response = true;
open_in_shell(path.as_path());
}
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of installation status object");
default_future(
Response::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file),
)
}

86
src/frontend/ui/mod.rs Normal file
View file

@ -0,0 +1,86 @@
//! frontend/ui/mod.rs
//!
//! Provides a web-view UI.
use anyhow::Result;
use wry::{
application::{
dpi::LogicalSize,
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Icon, WindowBuilder},
},
webview::{RpcResponse, WebViewBuilder},
};
use log::Level;
use crate::logging::LoggingErrors;
const ICON_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/icon-data.bin"));
/// Starts the main web UI. Will return when UI is closed.
pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) -> Result<()> {
#[cfg(windows)]
{
crate::native::prepare_install_webview2(app_name).log_expect("Unable to install webview2");
}
let size = if is_launcher {
(600.0, 300.0)
} else {
(1024.0, 600.0)
};
info!("Spawning web view instance");
let window_icon =
Icon::from_rgba(ICON_DATA.to_vec(), 48, 48).log_expect("Unable to construct window icon");
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title(format!("{} Installer", app_name))
.with_window_icon(Some(window_icon))
.with_inner_size(LogicalSize::new(size.0, size.1))
.with_resizable(false)
.build(&event_loop)?;
let _webview = WebViewBuilder::new(window)?
.with_url(http_address)?
.with_rpc_handler(|_, mut event| {
debug!("Incoming payload: {:?}", event);
match event.method.as_str() {
"Test" => (),
"Log" => {
if let Some(msg) = event.params.take() {
if let Ok(msg) = serde_json::from_value::<(String, String)>(msg) {
let kind = match msg.0.as_str() {
"info" | "log" => Level::Info,
"warn" => Level::Warn,
_ => Level::Error,
};
log!(target: "liftinstall::frontend::js", kind, "{}", msg.1);
}
}
}
"SelectInstallDir" => {
let result =
tinyfiledialogs::select_folder_dialog("Select a install directory...", "")
.and_then(|v| serde_json::to_value(v).ok());
return Some(RpcResponse::new_result(event.id, result));
}
_ => warn!("Unknown RPC method: {}", event.method),
}
None
})
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => info!("Webview started"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
}
});
}

View file

@ -7,14 +7,16 @@ use reqwest::header::CONTENT_LENGTH;
use std::io::Read;
use std::time::Duration;
use reqwest::r#async::Client as AsyncClient;
use reqwest::Client;
use reqwest::StatusCode;
/// Asserts that a URL is valid HTTPS, else returns an error.
pub fn assert_ssl(url: &str) -> Result<(), String> {
if url.starts_with("https://") {
Ok(())
} else {
Err(format!("Specified URL was not https"))
Err("Specified URL was not https".to_string())
}
}
@ -26,32 +28,49 @@ pub fn build_client() -> Result<Client, String> {
.map_err(|x| format!("Unable to build client: {:?}", x))
}
/// Downloads a text file from the specified URL.
pub fn download_text(url: &str) -> Result<String, String> {
assert_ssl(url)?;
let mut client = build_client()?
.get(url)
.send()
.map_err(|x| format!("Failed to GET resource: {:?}", x))?;
client
.text()
.map_err(|v| format!("Failed to get text from resource: {:?}", v))
/// Builds a customised async HTTP client.
pub fn build_async_client() -> Result<AsyncClient, String> {
AsyncClient::builder()
.timeout(Duration::from_secs(8))
.build()
.map_err(|x| format!("Unable to build client: {:?}", x))
}
/// Streams a file from a HTTP server.
pub fn stream_file<F>(url: &str, mut callback: F) -> Result<(), String>
pub fn stream_file<F>(
url: &str,
authorization: Option<String>,
mut callback: F,
) -> Result<(), String>
where
F: FnMut(Vec<u8>, u64) -> (),
{
assert_ssl(url)?;
let mut client = build_client()?
.get(url)
let mut client = build_client()?.get(url);
if let Some(auth) = authorization {
client = client.header("Authorization", format!("Bearer {}", auth));
}
let mut client = client
.send()
.map_err(|x| format!("Failed to GET resource: {:?}", x))?;
match client.status() {
StatusCode::OK => {}
StatusCode::TOO_MANY_REQUESTS => {
return Err(
"Your token has exceeded the number of daily allowable IP addresses. \
Please wait 24 hours and try again."
.to_string(),
);
}
x => {
return Err(format!("Bad status code: {:?}.", x));
}
}
let size = match client.headers().get(CONTENT_LENGTH) {
Some(ref v) => v
.to_str()

View file

@ -18,29 +18,32 @@ use std::sync::mpsc::Sender;
use std::io::copy;
use std::io::Cursor;
use std::process::exit;
use std::process::Command;
use std::process::{exit, Stdio};
use config::BaseAttributes;
use config::Config;
use crate::config::BaseAttributes;
use crate::config::Config;
use sources::types::Version;
use crate::sources::types::Version;
use tasks::install::InstallTask;
use tasks::uninstall::UninstallTask;
use tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
use tasks::DependencyTree;
use tasks::TaskMessage;
use crate::tasks::install::InstallTask;
use crate::tasks::uninstall::UninstallTask;
use crate::tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
use crate::tasks::DependencyTree;
use crate::tasks::TaskMessage;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use dirs::home_dir;
use std::collections::HashSet;
use std::fs::remove_file;
use http;
use crate::http;
use number_prefix::{decimal_prefix, Prefixed, Standalone};
use number_prefix::NumberPrefix::{self, Prefixed, Standalone};
use crate::native;
/// A message thrown during the installation of packages.
#[derive(Serialize)]
@ -48,14 +51,25 @@ pub enum InstallMessage {
Status(String, f64),
PackageInstalled,
Error(String),
AuthorizationRequired(String),
EOF,
}
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct Credentials {
#[serde(default)]
pub username: String,
#[serde(default)]
pub token: String,
}
/// Metadata about the current installation itself.
#[derive(Serialize, Deserialize, Clone)]
pub struct InstallationDatabase {
pub packages: Vec<LocalInstallation>,
pub shortcuts: Vec<String>,
#[serde(default)]
pub credentials: Credentials,
}
impl InstallationDatabase {
@ -64,6 +78,10 @@ impl InstallationDatabase {
InstallationDatabase {
packages: Vec::new(),
shortcuts: Vec::new(),
credentials: Credentials {
username: String::new(),
token: String::new(),
},
}
}
}
@ -80,6 +98,7 @@ pub struct InstallerFramework {
// If we just completed an uninstall, and we should clean up after ourselves.
pub burn_after_exit: bool,
pub launcher_path: Option<String>,
pub is_windows: bool,
}
/// Contains basic properties on the status of the session. Subset of InstallationFramework.
@ -100,19 +119,25 @@ pub struct LocalInstallation {
/// Relative paths to generated files
pub files: Vec<String>,
/// Absolute paths to generated shortcut files
pub shortcuts: Vec<String>,
pub shortcuts: HashSet<String>,
}
macro_rules! declare_messenger_callback {
($target:expr) => {
&|msg: &TaskMessage| match msg {
&TaskMessage::DisplayMessage(msg, progress) => {
&|msg: &TaskMessage| match *msg {
TaskMessage::DisplayMessage(msg, progress) => {
if let Err(v) = $target.send(InstallMessage::Status(msg.to_string(), progress as _))
{
error!("Failed to submit queue message: {:?}", v);
}
}
&TaskMessage::PackageInstalled => {
TaskMessage::AuthorizationRequired(msg) => {
if let Err(v) = $target.send(InstallMessage::AuthorizationRequired(msg.to_string()))
{
error!("Failed to submit queue message: {:?}", v);
}
}
TaskMessage::PackageInstalled => {
if let Err(v) = $target.send(InstallMessage::PackageInstalled) {
error!("Failed to submit queue message: {:?}", v);
}
@ -151,11 +176,14 @@ impl InstallerFramework {
/// items: Array of named packages to be installed/kept
/// messages: Channel used to send progress messages
/// fresh_install: If the install directory must be empty
/// force_install: If the install directory should be erased first
pub fn install(
&mut self,
items: Vec<String>,
messages: &Sender<InstallMessage>,
fresh_install: bool,
create_desktop_shortcuts: bool,
force_install: bool,
) -> Result<(), String> {
info!(
"Framework: Installing {:?} to {:?}",
@ -184,6 +212,8 @@ impl InstallerFramework {
items,
uninstall_items,
fresh_install,
create_desktop_shortcuts,
force_install,
});
let mut tree = DependencyTree::build(task);
@ -250,7 +280,7 @@ impl InstallerFramework {
let mut downloaded = 0;
let mut data_storage: Vec<u8> = Vec::new();
http::stream_file(tool, |data, size| {
http::stream_file(tool, None, |data, size| {
{
data_storage.extend_from_slice(&data);
}
@ -264,11 +294,11 @@ impl InstallerFramework {
};
// Pretty print data volumes
let pretty_current = match decimal_prefix(downloaded as f64) {
let pretty_current = match NumberPrefix::decimal(downloaded as f64) {
Standalone(bytes) => format!("{} bytes", bytes),
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
};
let pretty_total = match decimal_prefix(size as f64) {
let pretty_total = match NumberPrefix::decimal(size as f64) {
Standalone(bytes) => format!("{} bytes", bytes),
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
};
@ -328,7 +358,8 @@ impl InstallerFramework {
x.to_str()
.log_expect("Unable to convert argument to String")
.to_string()
}).collect();
})
.collect();
{
let new_app_file = match File::create(&args_file) {
@ -393,6 +424,29 @@ impl InstallerFramework {
}
}
/// Shuts down the installer instance.
pub fn shutdown(&mut self) -> Result<(), String> {
info!("Shutting down installer framework...");
if let Some(ref v) = self.launcher_path.take() {
info!("Launching {:?}", v);
Command::new(v)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|x| format!("Unable to start application: {:?}", x))?;
}
if self.burn_after_exit {
info!("Requesting that self be deleted after exit.");
native::burn_on_exit(&self.base_attributes.name);
self.burn_after_exit = false;
}
Ok(())
}
/// Creates a new instance of the Installer Framework with a specified Config.
pub fn new(attrs: BaseAttributes) -> Self {
InstallerFramework {
@ -404,6 +458,25 @@ impl InstallerFramework {
is_launcher: false,
burn_after_exit: false,
launcher_path: None,
is_windows: cfg!(windows),
}
}
/// The special recovery mode for the Installer Framework.
pub fn new_recovery_mode(attrs: BaseAttributes, install_path: &Path) -> Self {
InstallerFramework {
base_attributes: BaseAttributes {
recovery: true,
..attrs
},
config: None,
database: InstallationDatabase::new(),
install_path: Some(install_path.to_path_buf()),
preexisting_install: true,
is_launcher: false,
burn_after_exit: false,
launcher_path: None,
is_windows: cfg!(windows),
}
}
@ -431,6 +504,7 @@ impl InstallerFramework {
is_launcher: false,
burn_after_exit: false,
launcher_path: None,
is_windows: cfg!(windows),
})
}
}

View file

@ -17,7 +17,8 @@ pub fn setup_logger(file_name: String) -> Result<(), fern::InitError> {
record.level(),
message
))
}).level(log::LevelFilter::Info)
})
.level(log::LevelFilter::Info)
.chain(io::stdout())
.chain(fern::log_file(file_name)?)
.apply()?;

View file

@ -7,10 +7,7 @@
#![deny(unsafe_code)]
#![deny(missing_docs)]
#[cfg(windows)]
extern crate nfd;
extern crate web_view;
extern crate wry;
extern crate futures;
extern crate hyper;
@ -30,7 +27,7 @@ extern crate semver;
extern crate dirs;
extern crate tar;
extern crate xz_decom;
extern crate xz2;
extern crate zip;
extern crate fern;
@ -40,59 +37,44 @@ extern crate log;
extern crate chrono;
extern crate clap;
#[cfg(windows)]
extern crate widestring;
#[cfg(windows)]
extern crate winapi;
#[cfg(not(windows))]
extern crate slug;
#[cfg(not(windows))]
extern crate sysinfo;
extern crate jsonwebtoken as jwt;
extern crate base64;
mod archives;
mod assets;
mod config;
mod frontend;
mod http;
mod installer;
mod logging;
mod native;
mod rest;
mod self_update;
mod sources;
mod tasks;
use web_view::*;
use installer::InstallerFramework;
#[cfg(windows)]
use nfd::Response;
use rest::WebServer;
use std::net::TcpListener;
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::sync::RwLock;
use std::path::PathBuf;
use std::process::exit;
use std::process::Command;
use std::{thread, time};
use std::fs::remove_file;
use std::fs::File;
use logging::LoggingErrors;
use std::path::PathBuf;
use clap::App;
use clap::Arg;
use log::Level;
use config::BaseAttributes;
use std::fs;
use std::process::{exit, Command, Stdio};
static RAW_CONFIG: &'static str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
#[derive(Deserialize, Debug)]
enum CallbackType {
SelectInstallDir { callback_name: String },
Log { msg: String, kind: String },
}
const RAW_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
fn main() {
let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read");
@ -100,6 +82,7 @@ fn main() {
logging::setup_logger(format!("{}_installer.log", config.name))
.expect("Unable to setup logging!");
// Parse CLI arguments
let app_name = config.name.clone();
let app_about = format!("An interactive installer for {}", app_name);
@ -112,7 +95,8 @@ fn main() {
.value_name("TARGET")
.help("Launches the specified executable after checking for updates")
.takes_value(true),
).arg(
)
.arg(
Arg::with_name("swap")
.long("swap")
.value_name("TARGET")
@ -125,109 +109,43 @@ fn main() {
info!("{} installer", app_name);
// Handle self-updating if needed
let current_exe = std::env::current_exe().log_expect("Current executable could not be found");
let current_path = current_exe
.parent()
.log_expect("Parent directory of executable could not be found");
// Check to see if we are currently in a self-update
if let Some(to_path) = matches.value_of("swap") {
let to_path = PathBuf::from(to_path);
// Sleep a little bit to allow Windows to close the previous file handle
thread::sleep(time::Duration::from_millis(3000));
info!(
"Swapping installer from {} to {}",
current_exe.display(),
to_path.display()
);
// Attempt it a few times because Windows can hold a lock
for i in 1..=5 {
let swap_result = if cfg!(windows) {
use std::fs::copy;
copy(&current_exe, &to_path).map(|_x| ())
} else {
use std::fs::rename;
rename(&current_exe, &to_path)
};
match swap_result {
Ok(_) => break,
Err(e) => {
if i < 5 {
info!("Copy attempt failed: {:?}, retrying in 3 seconds.", e);
thread::sleep(time::Duration::from_millis(3000));
} else {
let _: () = Err(e).log_expect("Copying new binary failed");
}
}
}
}
Command::new(to_path)
.spawn()
.log_expect("Unable to start child process");
exit(0);
// Handle self-updating if needed
self_update::perform_swap(&current_exe, matches.value_of("swap"));
if let Some(new_matches) = self_update::check_args(reinterpret_app, current_path) {
matches = new_matches;
}
self_update::cleanup(current_path);
// If we just finished a update, we need to inject our previous command line arguments
let args_file = current_path.join("args.json");
if args_file.exists() {
let database: Vec<String> = {
let metadata_file =
File::open(&args_file).log_expect("Unable to open args file handle");
serde_json::from_reader(metadata_file).log_expect("Unable to read metadata file")
};
matches = reinterpret_app.get_matches_from(database);
info!("Parsed command line arguments from original instance");
remove_file(args_file).log_expect("Unable to clean up args file");
}
// Cleanup any remaining new maintenance tool instances if they exist
if cfg!(windows) {
let updater_executable = current_path.join("maintenancetool_new.exe");
if updater_executable.exists() {
// Sleep a little bit to allow Windows to close the previous file handle
thread::sleep(time::Duration::from_millis(3000));
// Attempt it a few times because Windows can hold a lock
for i in 1..=5 {
let swap_result = remove_file(&updater_executable);
match swap_result {
Ok(_) => break,
Err(e) => {
if i < 5 {
info!("Cleanup attempt failed: {:?}, retrying in 3 seconds.", e);
thread::sleep(time::Duration::from_millis(3000));
} else {
warn!("Deleting temp binary failed after 5 attempts: {:?}", e);
}
}
}
}
}
}
// Load in metadata as to learn about the environment
// Load in metadata + setup the installer framework
let mut fresh_install = false;
let metadata_file = current_path.join("metadata.json");
let mut framework = if metadata_file.exists() {
info!("Using pre-existing metadata file: {:?}", metadata_file);
InstallerFramework::new_with_db(config, current_path).log_expect("Unable to parse metadata")
InstallerFramework::new_with_db(config.clone(), current_path).unwrap_or_else(|e| {
error!("Failed to load metadata: {:?}", e);
warn!("Entering recovery mode");
InstallerFramework::new_recovery_mode(config, current_path)
})
} else {
info!("Starting fresh install");
fresh_install = true;
InstallerFramework::new(config)
};
// check for existing installs if we are running as a fresh install
let installed_path = PathBuf::from(framework.get_default_path().unwrap());
if fresh_install && installed_path.join("metadata.json").exists() {
info!("Existing install detected! Copying Trying to launch this install instead");
// Ignore the return value from this since it should exit the application if its successful
let _ = replace_existing_install(&current_exe, &installed_path);
}
let is_launcher = if let Some(string) = matches.value_of("launcher") {
framework.is_launcher = true;
framework.launcher_path = Some(string.to_string());
@ -236,97 +154,53 @@ fn main() {
false
};
// Firstly, allocate us an epidermal port
let target_port = {
let listener = TcpListener::bind("127.0.0.1:0")
.log_expect("At least one local address should be free");
listener
.local_addr()
.log_expect("Should be able to pull address from listener")
.port()
// Start up the UI
frontend::launch(&app_name, is_launcher, framework);
}
fn replace_existing_install(current_exe: &PathBuf, installed_path: &PathBuf) -> Result<(), String> {
// Generate installer path
let platform_extension = if cfg!(windows) {
"maintenancetool.exe"
} else {
"maintenancetool"
};
// Now, iterate over all ports
let addresses = "localhost:0"
.to_socket_addrs()
.log_expect("No localhost address found");
let new_tool = if cfg!(windows) {
"maintenancetool_new.exe"
} else {
"maintenancetool_new"
};
let mut servers = Vec::new();
let mut http_address = None;
let framework = Arc::new(RwLock::new(framework));
// Startup HTTP server for handling the web view
for mut address in addresses {
address.set_port(target_port);
let server = WebServer::with_addr(framework.clone(), address)
.log_expect("Failed to bind to address");
info!("Server: {:?}", address);
http_address = Some(address);
servers.push(server);
if let Err(v) = fs::copy(current_exe, installed_path.join(new_tool)) {
return Err(format!("Unable to copy installer binary: {:?}", v));
}
let http_address = http_address.log_expect("No HTTP address found");
let existing = installed_path
.join(platform_extension)
.into_os_string()
.into_string();
let new = installed_path.join(new_tool).into_os_string().into_string();
if existing.is_ok() && new.is_ok() {
// Remove NTFS alternate stream which tells the operating system that the updater was downloaded from the internet
if cfg!(windows) {
let _ = fs::remove_file(
installed_path.join("maintenancetool_new.exe:Zone.Identifier:$DATA"),
);
}
info!("Launching {:?}", existing);
let success = Command::new(new.unwrap())
.arg("--swap")
.arg(existing.unwrap())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
if success.is_ok() {
exit(0);
} else {
error!("Unable to start existing yuzu maintenance tool. Launching old one instead");
}
}
let http_address = format!("http://localhost:{}", http_address.port());
// Init the web view
let size = if is_launcher { (600, 300) } else { (1024, 500) };
let resizable = false;
let debug = true;
run(
&format!("{} Installer", app_name),
Content::Url(http_address),
Some(size),
resizable,
debug,
|_| {},
|wv, msg, _| {
let command: CallbackType =
serde_json::from_str(msg).log_expect(&format!("Unable to parse string: {:?}", msg));
debug!("Incoming payload: {:?}", command);
match command {
CallbackType::SelectInstallDir { callback_name } => {
#[cfg(windows)]
let result = match nfd::open_pick_folder(None)
.log_expect("Unable to open folder dialog")
{
Response::Okay(v) => v,
_ => return,
};
#[cfg(not(windows))]
let result =
wv.dialog(Dialog::ChooseDirectory, "Select a install directory...", "");
if !result.is_empty() {
let result = serde_json::to_string(&result)
.log_expect("Unable to serialize response");
let command = format!("{}({});", callback_name, result);
debug!("Injecting response: {}", command);
wv.eval(&command);
}
}
CallbackType::Log { msg, kind } => {
let kind = match kind.as_ref() {
"info" | "log" => Level::Info,
"warn" => Level::Warn,
"error" => Level::Error,
_ => Level::Error,
};
log!(target: "liftinstall::frontend-js", kind, "{}", msg);
}
}
},
(),
);
Ok(())
}

View file

@ -2,76 +2,107 @@
* Misc interop helpers.
**/
// Explicitly use the Unicode version of the APIs
#ifndef UNICODE
#define UNICODE
#endif
#ifndef _UNICODE
#define _UNICODE
#endif
#include "windows.h"
#include "winnls.h"
#include "shobjidl.h"
#include "objbase.h"
#include "objidl.h"
#include "shlguid.h"
#include "shlobj.h"
// https://stackoverflow.com/questions/52101827/windows-10-getsyscolor-does-not-get-dark-ui-color-theme
extern "C" int isDarkThemeActive()
{
DWORD type;
DWORD value;
DWORD count = 4;
LSTATUS st = RegGetValue(
HKEY_CURRENT_USER,
TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"),
TEXT("AppsUseLightTheme"),
RRF_RT_REG_DWORD,
&type,
&value,
&count);
if (st == ERROR_SUCCESS && type == REG_DWORD)
return value == 0;
return false;
}
extern "C" int saveShortcut(
const char *shortcutPath,
const char *description,
const char *path,
const char *args,
const char *workingDir) {
char* errStr = NULL;
const wchar_t *shortcutPath,
const wchar_t *description,
const wchar_t *path,
const wchar_t *args,
const wchar_t *workingDir,
const wchar_t *exePath)
{
char *errStr = NULL;
HRESULT h;
IShellLink* shellLink = NULL;
IPersistFile* persistFile = NULL;
#ifdef _WIN64
wchar_t wName[MAX_PATH+1];
#else
WORD wName[MAX_PATH+1];
#endif
int id;
IShellLink *shellLink = NULL;
IPersistFile *persistFile = NULL;
// Initialize the COM library
h = CoInitialize(NULL);
if (FAILED(h)) {
if (FAILED(h))
{
errStr = "Failed to initialize COM library";
goto err;
}
h = CoCreateInstance( CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
IID_IShellLink, (PVOID*)&shellLink );
if (FAILED(h)) {
h = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
IID_IShellLink, (PVOID *)&shellLink);
if (FAILED(h))
{
errStr = "Failed to create IShellLink";
goto err;
}
h = shellLink->QueryInterface(IID_IPersistFile, (PVOID*)&persistFile);
if (FAILED(h)) {
h = shellLink->QueryInterface(IID_IPersistFile, (PVOID *)&persistFile);
if (FAILED(h))
{
errStr = "Failed to get IPersistFile";
goto err;
}
//Append the shortcut name to the folder
MultiByteToWideChar(CP_UTF8,0,shortcutPath,-1,wName,MAX_PATH);
// Load the file if it exists, to get the values for anything
// that we do not set. Ignore errors, such as if it does not exist.
h = persistFile->Load(wName, 0);
h = persistFile->Load(shortcutPath, 0);
// Set the fields for which the application has set a value
if (description!=NULL)
if (description != NULL)
shellLink->SetDescription(description);
if (path!=NULL)
if (path != NULL)
shellLink->SetPath(path);
if (args!=NULL)
// default to using the first icon in the exe (usually correct)
if (exePath != NULL)
shellLink->SetIconLocation(exePath, 0);
if (args != NULL)
shellLink->SetArguments(args);
if (workingDir!=NULL)
if (workingDir != NULL)
shellLink->SetWorkingDirectory(workingDir);
//Save the shortcut to disk
h = persistFile->Save(wName, TRUE);
if (FAILED(h)) {
h = persistFile->Save(shortcutPath, TRUE);
if (FAILED(h))
{
errStr = "Failed to save shortcut";
goto err;
}
// Notify that a new shortcut was created using the shell api
SHChangeNotify(SHCNE_CREATE, SHCNF_PATH, shortcutPath, NULL);
SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATH, shortcutPath, NULL);
persistFile->Release();
shellLink->Release();
CoUninitialize();
@ -87,3 +118,65 @@ err:
return h;
}
extern "C" int spawnDetached(const wchar_t *app, const wchar_t *cmdline)
{
STARTUPINFOW si;
PROCESS_INFORMATION pi;
// make non-constant copy of the parameters
// this is allowed per https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessw#security-remarks
wchar_t *app_copy = _wcsdup(app);
wchar_t *cmdline_copy = _wcsdup(cmdline);
if (app_copy == NULL || cmdline_copy == NULL)
{
return GetLastError();
}
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcessW(app, // module name
(LPWSTR)cmdline, // Command line, unicode is allowed
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
CREATE_NO_WINDOW, // Create without window
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi) // Pointer to PROCESS_INFORMATION structure
)
{
return GetLastError();
}
// Close process and thread handles.
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
extern "C" HRESULT getSystemFolder(wchar_t *out_path)
{
PWSTR path = NULL;
HRESULT result = SHGetKnownFolderPath(FOLDERID_System, 0, NULL, &path);
if (result == S_OK)
{
wcscpy_s(out_path, MAX_PATH + 1, path);
CoTaskMemFree(path);
}
return result;
}
extern "C" HRESULT getDesktopFolder(wchar_t *out_path)
{
PWSTR path = NULL;
HRESULT result = SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &path);
if (result == S_OK)
{
wcscpy_s(out_path, MAX_PATH + 1, path);
CoTaskMemFree(path);
}
return result;
}

View file

@ -3,8 +3,8 @@
/// Basic definition of some running process.
#[derive(Debug)]
pub struct Process {
pub pid : usize,
pub name : String
pub pid: usize,
pub name: String,
}
#[cfg(windows)]
@ -14,32 +14,82 @@ mod natives {
#![allow(non_snake_case)]
const PROCESS_LEN: usize = 10192;
const WV2_INSTALLER_DATA: &[u8] = include_bytes!("../../MicrosoftEdgeWebview2Setup.exe");
use std::ffi::CString;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use std::env;
use std::process::Command;
use std::io::Write;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use winapi::shared::minwindef::{DWORD, FALSE, MAX_PATH};
use winapi::shared::winerror::HRESULT;
use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::psapi::{
EnumProcessModulesEx, GetModuleFileNameExW, K32EnumProcesses, LIST_MODULES_ALL,
};
use winapi::um::shellapi::ShellExecuteW;
use winapi::um::winnt::{
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
};
use winapi::um::processthreadsapi::{OpenProcess};
use winapi::um::psapi::{
K32EnumProcesses,
EnumProcessModulesEx, GetModuleFileNameExW, LIST_MODULES_ALL,
};
use winapi::um::winuser::SW_SHOWDEFAULT;
use std::process::Command;
use tempfile::Builder;
use tinyfiledialogs::{message_box_yes_no, MessageBoxIcon, YesNo};
use webview2::EnvironmentBuilder;
use widestring::U16CString;
extern "C" {
pub fn saveShortcut(
shortcutPath: *const ::std::os::raw::c_char,
description: *const ::std::os::raw::c_char,
path: *const ::std::os::raw::c_char,
args: *const ::std::os::raw::c_char,
workingDir: *const ::std::os::raw::c_char,
shortcutPath: *const winapi::ctypes::wchar_t,
description: *const winapi::ctypes::wchar_t,
path: *const winapi::ctypes::wchar_t,
args: *const winapi::ctypes::wchar_t,
workingDir: *const winapi::ctypes::wchar_t,
exePath: *const winapi::ctypes::wchar_t,
) -> ::std::os::raw::c_int;
pub fn isDarkThemeActive() -> ::std::os::raw::c_uint;
pub fn spawnDetached(
app: *const winapi::ctypes::wchar_t,
cmdline: *const winapi::ctypes::wchar_t,
) -> ::std::os::raw::c_int;
pub fn getSystemFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
pub fn getDesktopFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
}
pub fn prepare_install_webview2(name: &str) -> Result<(), String> {
if EnvironmentBuilder::default()
.get_available_browser_version_string()
.is_ok()
{
return Ok(());
}
if message_box_yes_no(&format!("{} installer", name), &format!("{} installer now requires Webview2 runtime to function properly.\nDo you wish to install it now?", name), MessageBoxIcon::Question, YesNo::Yes) == YesNo::No {
std::process::exit(1);
}
let mut installer_file = Builder::new()
.suffix(".exe")
.tempfile()
.log_expect("Unable to open the webview2 installer file");
installer_file
.write_all(&WV2_INSTALLER_DATA)
.log_expect("Unable to write the webview2 installer file");
let path = installer_file.path().to_owned();
installer_file.keep().log_unwrap();
Command::new(&path)
.arg("/install")
.spawn()
.log_expect("Unable to run the webview2 installer")
.wait()
.log_unwrap();
Ok(())
}
// Needed here for Windows interop
@ -50,24 +100,77 @@ mod natives {
target: &str,
args: &str,
working_dir: &str,
exe_path: &str,
) -> Result<String, String> {
let source_file = format!(
"{}\\Microsoft\\Windows\\Start Menu\\Programs\\{}.lnk",
env::var("APPDATA").log_expect("APPDATA is bad, apparently"),
name
);
create_shortcut_inner(
source_file,
name,
description,
target,
args,
working_dir,
exe_path,
)
}
// Needed here for Windows interop
#[allow(unsafe_code)]
pub fn create_desktop_shortcut(
name: &str,
description: &str,
target: &str,
args: &str,
working_dir: &str,
exe_path: &str,
) -> Result<String, String> {
let mut cmd_path = [0u16; MAX_PATH + 1];
let _result = unsafe { getDesktopFolder(cmd_path.as_mut_ptr()) };
let source_path = format!(
"{}\\{}.lnk",
String::from_utf16_lossy(&cmd_path[..count_u16(&cmd_path)]).as_str(),
name
);
create_shortcut_inner(
source_path,
name,
description,
target,
args,
working_dir,
exe_path,
)
}
// Needed here for Windows interop
#[allow(unsafe_code)]
fn create_shortcut_inner(
source_file: String,
_name: &str,
description: &str,
target: &str,
args: &str,
working_dir: &str,
exe_path: &str,
) -> Result<String, String> {
info!("Generating shortcut @ {:?}", source_file);
let native_target_dir = CString::new(source_file.clone())
.log_expect("Error while converting to C-style string");
let native_target_dir = U16CString::from_str(source_file.clone())
.log_expect("Error while converting to wchar_t");
let native_description =
CString::new(description).log_expect("Error while converting to C-style string");
U16CString::from_str(description).log_expect("Error while converting to wchar_t");
let native_target =
CString::new(target).log_expect("Error while converting to C-style string");
let native_args = CString::new(args).log_expect("Error while converting to C-style string");
U16CString::from_str(target).log_expect("Error while converting to wchar_t");
let native_args =
U16CString::from_str(args).log_expect("Error while converting to wchar_t");
let native_working_dir =
CString::new(working_dir).log_expect("Error while converting to C-style string");
U16CString::from_str(working_dir).log_expect("Error while converting to wchar_t");
let native_exe_path =
U16CString::from_str(exe_path).log_expect("Error while converting to wchar_t");
let shortcutResult = unsafe {
saveShortcut(
@ -76,6 +179,7 @@ mod natives {
native_target.as_ptr(),
native_args.as_ptr(),
native_working_dir.as_ptr(),
native_exe_path.as_ptr(),
)
};
@ -88,6 +192,37 @@ mod natives {
}
}
// Needed to call unsafe function `ShellExecuteW` from `winapi` crate
#[allow(unsafe_code)]
pub fn open_in_shell(path: &Path) {
let native_verb = U16CString::from_str("open").unwrap();
// https://doc.rust-lang.org/std/os/windows/ffi/trait.OsStrExt.html#tymethod.encode_wide
let mut native_path: Vec<u16> = path.as_os_str().encode_wide().collect();
native_path.push(0); // NULL terminator
unsafe {
ShellExecuteW(
std::ptr::null_mut(),
native_verb.as_ptr(),
native_path.as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
SW_SHOWDEFAULT,
);
}
}
#[inline]
fn count_u16(u16str: &[u16]) -> usize {
let mut pos = 0;
for x in u16str.iter() {
if *x == 0 {
break;
}
pos += 1;
}
pos
}
/// Cleans up the installer
pub fn burn_on_exit(app_name: &str) {
let current_exe = env::current_exe().log_expect("Current executable could not be found");
@ -101,6 +236,7 @@ mod natives {
.to_str()
.log_expect("Unable to convert tool path to string")
.replace(" ", "\\ ");
let tool_wv = format!("{}.WebView2", tool);
let log = path.join(format!("{}_installer.log", app_name));
let log = log
@ -108,15 +244,49 @@ mod natives {
.log_expect("Unable to convert log path to string")
.replace(" ", "\\ ");
let target_arguments = format!("ping 127.0.0.1 -n 3 > nul && del {} {}", tool, log);
let install_path = path
.to_str()
.log_expect("Unable to convert path to string")
.replace(" ", "\\ ");
let target_arguments = format!(
"/C choice /C Y /N /D Y /T 2 & del {} {} & rmdir /Q /S {} & rmdir {}",
tool, log, tool_wv, install_path
);
info!("Launching cmd with {:?}", target_arguments);
Command::new("C:\\Windows\\system32\\cmd.exe")
.arg("/C")
.arg(&target_arguments)
.spawn()
.log_expect("Unable to start child process");
// Needs to use `spawnDetached` which is an unsafe C/C++ function from interop.cpp
#[allow(unsafe_code)]
let spawn_result: i32 = unsafe {
let mut cmd_path = [0u16; MAX_PATH + 1];
let result = getSystemFolder(cmd_path.as_mut_ptr());
let mut pos = 0;
for x in cmd_path.iter() {
if *x == 0 {
break;
}
pos += 1;
}
if result != winapi::shared::winerror::S_OK {
return;
}
spawnDetached(
U16CString::from_str(
format!("{}\\cmd.exe", String::from_utf16_lossy(&cmd_path[..pos])).as_str(),
)
.log_expect("Unable to convert string to wchar_t")
.as_ptr(),
U16CString::from_str(target_arguments.as_str())
.log_expect("Unable to convert string to wchar_t")
.as_ptr(),
)
};
if spawn_result != 0 {
warn!("Unable to start child process");
}
}
#[allow(unsafe_code)]
@ -149,9 +319,7 @@ mod natives {
let size = ::std::mem::size_of::<DWORD>() * process_ids.len();
unsafe {
if K32EnumProcesses(process_ids.as_mut_ptr(),
size as DWORD,
&mut cb_needed) == 0 {
if K32EnumProcesses(process_ids.as_mut_ptr(), size as DWORD, &mut cb_needed) == 0 {
return vec![];
}
}
@ -160,7 +328,7 @@ mod natives {
let mut processes = Vec::new();
for i in 0 .. nb_processes {
for i in 0..nb_processes {
let pid = process_ids[i as usize];
unsafe {
@ -169,20 +337,25 @@ mod natives {
let mut process_name = [0u16; MAX_PATH + 1];
let mut cb_needed = 0;
if EnumProcessModulesEx(process_handler,
&mut h_mod,
::std::mem::size_of::<DWORD>() as DWORD,
&mut cb_needed,
LIST_MODULES_ALL) != 0 {
GetModuleFileNameExW(process_handler,
h_mod,
process_name.as_mut_ptr(),
MAX_PATH as DWORD + 1);
if EnumProcessModulesEx(
process_handler,
&mut h_mod,
::std::mem::size_of::<DWORD>() as DWORD,
&mut cb_needed,
LIST_MODULES_ALL,
) != 0
{
GetModuleFileNameExW(
process_handler,
h_mod,
process_name.as_mut_ptr(),
MAX_PATH as DWORD + 1,
);
let mut pos = 0;
for x in process_name.iter() {
if *x == 0 {
break
break;
}
pos += 1;
}
@ -199,51 +372,143 @@ mod natives {
processes
}
// Needed here for Windows interop
#[allow(unsafe_code)]
pub fn is_dark_mode_active() -> bool {
unsafe { isDarkThemeActive() == 1 }
}
}
#[cfg(not(windows))]
mod natives {
use std::fs::remove_file;
use std::fs::{remove_dir, remove_file};
use std::env;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use sysinfo::{PidExt, ProcessExt, SystemExt};
use dirs;
use slug::slugify;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::Path;
use std::process::Command;
#[cfg(target_os = "linux")]
pub fn create_shortcut(
name: &str,
description: &str,
target: &str,
args: &str,
working_dir: &str,
exe_path: &str,
) -> Result<String, String> {
// TODO: no-op
warn!("create_shortcut is stubbed!");
// FIXME: no icon will be shown since no icon is provided
let data_local_dir = dirs::data_local_dir();
match data_local_dir {
Some(x) => {
let mut path = x;
path.push("applications");
match create_dir_all(path.to_path_buf()) {
Ok(_) => (),
Err(e) => {
return Err(format!(
"Local data directory does not exist and cannot be created: {}",
e
));
}
};
path.push(format!("yuzu-maintenance-tool_{}.desktop", slugify(name))); // file name
let desktop_file = format!(
"[Desktop Entry]\nType=Application\nName={}\nExec=\"{}\" {}\nComment={}\nPath={}\nIcon=yuzu\n",
name, target, args, description, working_dir
);
let desktop_f = File::create(path);
let mut desktop_f = match desktop_f {
Ok(file) => file,
Err(e) => return Err(format!("Unable to create desktop file: {}", e)),
};
let desktop_f = desktop_f.write_all(desktop_file.as_bytes());
match desktop_f {
Ok(_) => Ok("".to_string()),
Err(e) => Err(format!("Unable to write desktop file: {}", e)),
}
}
// return error when failed to acquire local data directory
None => Err("Unable to determine local data directory".to_string()),
}
}
#[cfg(target_os = "macos")]
pub fn create_shortcut(
name: &str,
description: &str,
target: &str,
args: &str,
working_dir: &str,
_exe_path: &str,
) -> Result<String, String> {
warn!("STUB! Creating shortcut is not implemented on macOS");
Ok("".to_string())
}
pub fn open_in_shell(path: &Path) {
let shell: &str;
if cfg!(target_os = "linux") {
shell = "xdg-open";
} else if cfg!(target_os = "macos") {
shell = "open";
} else {
warn!("Unsupported platform");
return;
}
Command::new(shell).arg(path).spawn().ok();
}
/// Cleans up the installer
pub fn burn_on_exit(app_name: &str) {
let current_exe = env::current_exe().log_expect("Current executable could not be found");
let exe_dir = current_exe
.parent()
.log_expect("Current executable directory cannot be found");
if let Err(e) = remove_file(exe_dir.join(format!("{}_installer.log", app_name))) {
// No regular logging now.
eprintln!("Failed to delete maintenance log: {:?}", e);
};
// Thank god for *nix platforms
if let Err(e) = remove_file(&current_exe) {
// No regular logging now.
eprintln!("Failed to delete maintenancetool: {:?}", e);
};
let current_dir = env::current_dir().log_expect("Current directory cannot be found");
if let Err(e) = remove_file(current_dir.join(format!("{}_installer.log", app_name))) {
// No regular logging now.
eprintln!("Failed to delete installer log: {:?}", e);
};
// delete the directory if not empty and ignore errors (since we can't handle errors anymore)
remove_dir(exe_dir).ok();
}
/// Returns a list of running processes
pub fn get_process_names() -> Vec<super::Process> {
// TODO: no-op
vec![]
// a platform-independent implementation using sysinfo crate
let mut processes: Vec<super::Process> = Vec::new();
let mut system = sysinfo::System::new();
system.refresh_all();
for (pid, procs) in system.processes() {
processes.push(super::Process {
pid: pid.as_u32() as usize,
name: procs.name().to_string(),
});
}
processes // return running processes
}
/// Returns if dark mode is active on this system.
pub fn is_dark_mode_active() -> bool {
// No-op
false
}
}

View file

@ -1,473 +0,0 @@
//! rest.rs
//!
//! Provides a HTTP/REST server for both frontend<->backend communication, as well
//! as talking to external applications.
use serde_json;
use futures::future;
use futures::Future;
use futures::Sink;
use futures::Stream;
use hyper::header::{ContentLength, ContentType};
use hyper::server::{Http, Request, Response, Service};
use hyper::{self, Error as HyperError, Get, Post, StatusCode};
use url::form_urlencoded;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::process::exit;
use std::process::Command;
use std::process::Stdio;
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::sync::RwLock;
use std::thread::{self, JoinHandle};
use assets;
use installer::InstallMessage;
use installer::InstallerFramework;
use logging::LoggingErrors;
use http;
use config::Config;
use native;
#[derive(Serialize)]
struct FileSelection {
path: Option<String>,
}
/// Acts as a communication mechanism between the Hyper WebService and the rest of the
/// application.
pub struct WebServer {
_handle: JoinHandle<()>,
}
impl WebServer {
/// Creates a new web server with the specified address.
pub fn with_addr(
framework: Arc<RwLock<InstallerFramework>>,
addr: SocketAddr,
) -> Result<Self, HyperError> {
let handle = thread::spawn(move || {
let server = Http::new()
.bind(&addr, move || {
Ok(WebService {
framework: framework.clone(),
})
}).log_expect("Failed to bind to port");
server.run().log_expect("Failed to run HTTP server");
});
Ok(WebServer { _handle: handle })
}
}
/// Holds internal state for Hyper
struct WebService {
framework: Arc<RwLock<InstallerFramework>>,
}
impl Service for WebService {
type Request = Request;
type Response = Response;
type Error = hyper::Error;
type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
/// HTTP request handler
fn call(&self, req: Self::Request) -> Self::Future {
Box::new(future::ok(match (req.method(), req.path()) {
// This endpoint should be usable directly from a <script> tag during loading.
(&Get, "/api/attrs") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let file = encapsulate_json(
"base_attributes",
&framework
.base_attributes
.to_json_str()
.log_expect("Failed to render JSON representation of config"),
);
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
// Returns the web config loaded
(&Get, "/api/config") => {
let mut framework = self
.framework
.write()
.log_expect("InstallerFramework has been dirtied");
info!(
"Downloading configuration from {:?}...",
framework.base_attributes.target_url
);
match http::download_text(&framework.base_attributes.target_url)
.map(|x| Config::from_toml_str(&x))
{
Ok(Ok(config)) => {
framework.config = Some(config.clone());
info!("Configuration file downloaded successfully.");
let file = framework
.get_config()
.log_expect("Config should be loaded by now")
.to_json_str()
.log_expect("Failed to render JSON representation of config");
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
Ok(Err(v)) => {
error!("Bad configuration file: {:?}", v);
Response::<hyper::Body>::new()
.with_status(StatusCode::ServiceUnavailable)
.with_header(ContentType::plaintext())
.with_body("Bad HTTP response")
}
Err(v) => {
error!(
"General connectivity error while downloading config: {:?}",
v
);
Response::<hyper::Body>::new()
.with_status(StatusCode::ServiceUnavailable)
.with_header(ContentLength(v.len() as u64))
.with_header(ContentType::plaintext())
.with_body(v)
}
}
}
// This endpoint should be usable directly from a <script> tag during loading.
(&Get, "/api/packages") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let file = encapsulate_json(
"packages",
&serde_json::to_string(&framework.database)
.log_expect("Failed to render JSON representation of database"),
);
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
// Returns the default path for a installation
(&Get, "/api/default-path") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let path = framework.get_default_path();
let response = FileSelection { path };
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of default path object");
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
// Immediately exits the application
(&Get, "/api/exit") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
if let Some(ref v) = framework.launcher_path {
Command::new(v)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.log_expect("Unable to start child process");
}
if framework.burn_after_exit {
native::burn_on_exit(&framework.base_attributes.name);
}
exit(0);
}
// Gets properties such as if the application is in maintenance mode
(&Get, "/api/installation-status") => {
let framework = self
.framework
.read()
.log_expect("InstallerFramework has been dirtied");
let response = framework.get_installation_status();
let file = serde_json::to_string(&response)
.log_expect("Failed to render JSON payload of installation status object");
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::json())
.with_body(file)
}
// Streams the installation of a particular set of packages
(&Post, "/api/uninstall") => {
// We need to bit of pipelining to get this to work
let framework = self.framework.clone();
return Box::new(req.body().concat2().map(move |_b| {
let (sender, receiver) = channel();
let (tx, rx) = hyper::Body::pair();
// Startup a thread to do this operation for us
thread::spawn(move || {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
if let Err(v) = framework.uninstall(&sender) {
error!("Uninstall error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send uninstall error: {:?}", v);
};
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
});
// Spawn a thread for transforming messages to chunk messages
thread::spawn(move || {
let mut tx = tx;
loop {
let response = receiver
.recv()
.log_expect("Failed to receive message from runner thread");
if let InstallMessage::EOF = response {
break;
}
let mut response = serde_json::to_string(&response)
.log_expect("Failed to render JSON logging response payload");
response.push('\n');
tx = tx
.send(Ok(response.into_bytes().into()))
.wait()
.log_expect("Failed to write JSON response payload to client");
}
});
Response::<hyper::Body>::new()
//.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::plaintext())
.with_body(rx)
}));
}
// Updates the installer
(&Post, "/api/update-updater") => {
// We need to bit of pipelining to get this to work
let framework = self.framework.clone();
return Box::new(req.body().concat2().map(move |_b| {
let (sender, receiver) = channel();
let (tx, rx) = hyper::Body::pair();
// Startup a thread to do this operation for us
thread::spawn(move || {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
if let Err(v) = framework.update_updater(&sender) {
error!("Self-update error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send self-update error: {:?}", v);
};
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
});
// Spawn a thread for transforming messages to chunk messages
thread::spawn(move || {
let mut tx = tx;
loop {
let response = receiver
.recv()
.log_expect("Failed to receive message from runner thread");
if let InstallMessage::EOF = response {
break;
}
let mut response = serde_json::to_string(&response)
.log_expect("Failed to render JSON logging response payload");
response.push('\n');
tx = tx
.send(Ok(response.into_bytes().into()))
.wait()
.log_expect("Failed to write JSON response payload to client");
}
});
Response::<hyper::Body>::new()
//.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::plaintext())
.with_body(rx)
}));
}
// Streams the installation of a particular set of packages
(&Post, "/api/start-install") => {
// We need to bit of pipelining to get this to work
let framework = self.framework.clone();
return Box::new(req.body().concat2().map(move |b| {
let results = form_urlencoded::parse(b.as_ref())
.into_owned()
.collect::<HashMap<String, String>>();
let mut to_install = Vec::new();
let mut path: Option<String> = None;
// Transform results into just an array of stuff to install
for (key, value) in &results {
if key == "path" {
path = Some(value.to_owned());
continue;
}
if value == "true" {
to_install.push(key.to_owned());
}
}
// The frontend always provides this
let path = path.log_expect(
"No path specified by frontend when one should have already existed",
);
let (sender, receiver) = channel();
let (tx, rx) = hyper::Body::pair();
// Startup a thread to do this operation for us
thread::spawn(move || {
let mut framework = framework
.write()
.log_expect("InstallerFramework has been dirtied");
let new_install = !framework.preexisting_install;
if new_install {
framework.set_install_dir(&path);
}
if let Err(v) = framework.install(to_install, &sender, new_install) {
error!("Install error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send install error: {:?}", v);
}
}
if let Err(v) = sender.send(InstallMessage::EOF) {
error!("Failed to send EOF to client: {:?}", v);
}
});
// Spawn a thread for transforming messages to chunk messages
thread::spawn(move || {
let mut tx = tx;
loop {
let mut panic_after_finish = false;
let response = match receiver
.recv() {
Ok(v) => v,
Err(v) => {
error!("Queue message failed: {:?}", v);
panic_after_finish = true;
InstallMessage::Error("Internal error".to_string())
}
};
if let InstallMessage::EOF = response {
break;
}
let mut response = serde_json::to_string(&response)
.log_expect("Failed to render JSON logging response payload");
response.push('\n');
tx = tx
.send(Ok(response.into_bytes().into()))
.wait()
.log_expect("Failed to write JSON response payload to client");
if panic_after_finish {
panic!("Failed to read from queue (flushed error message successfully)");
}
}
});
Response::<hyper::Body>::new()
//.with_header(ContentLength(file.len() as u64))
.with_header(ContentType::plaintext())
.with_body(rx)
}));
}
// Static file handler
(&Get, _) => {
// At this point, we have a web browser client. Search for a index page
// if needed
let mut path: String = req.path().to_owned();
if path.ends_with('/') {
path += "index.html";
}
match assets::file_from_string(&path) {
Some((content_type, file)) => {
let content_type = ContentType(content_type.parse().log_expect(
"Failed to parse content type into correct representation",
));
Response::<hyper::Body>::new()
.with_header(ContentLength(file.len() as u64))
.with_header(content_type)
.with_body(file)
}
None => Response::new().with_status(StatusCode::NotFound),
}
}
// Fallthrough for POST/PUT/CONNECT/...
_ => Response::new().with_status(StatusCode::NotFound),
}))
}
}
/// Encapsulates JSON as a injectable Javascript script.
fn encapsulate_json(field_name: &str, json: &str) -> String {
format!("var {} = {};", field_name, json)
}

111
src/self_update.rs Normal file
View file

@ -0,0 +1,111 @@
//! self_update.rs
//!
//! Handles different components of self-updating.
use std::fs::{remove_file, File};
use std::path::{Path, PathBuf};
use std::process::{exit, Command};
use std::{thread, time};
use clap::{App, ArgMatches};
use crate::logging::LoggingErrors;
/// Swaps around the main executable if needed.
pub fn perform_swap(current_exe: &PathBuf, to_path: Option<&str>) {
// Check to see if we are currently in a self-update
if let Some(to_path) = to_path {
let to_path = PathBuf::from(to_path);
// Sleep a little bit to allow Windows to close the previous file handle
thread::sleep(time::Duration::from_millis(3000));
info!(
"Swapping installer from {} to {}",
current_exe.display(),
to_path.display()
);
// Attempt it a few times because Windows can hold a lock
for i in 1..=5 {
let swap_result = if cfg!(windows) {
use std::fs::copy;
copy(&current_exe, &to_path).map(|_x| ())
} else {
use std::fs::rename;
rename(&current_exe, &to_path)
};
match swap_result {
Ok(_) => break,
Err(e) => {
if i < 5 {
info!("Copy attempt failed: {:?}, retrying in 3 seconds.", e);
thread::sleep(time::Duration::from_millis(3000));
} else {
Err::<(), _>(e).log_expect("Copying new binary failed");
}
}
}
}
Command::new(to_path)
.spawn()
.log_expect("Unable to start child process");
exit(0);
}
}
pub fn check_args<'a>(app: App<'a, '_>, current_path: &Path) -> Option<ArgMatches<'a>> {
// If we just finished a update, we need to inject our previous command line arguments
let args_file = current_path.join("args.json");
if args_file.exists() {
let database: Vec<String> = {
let metadata_file =
File::open(&args_file).log_expect("Unable to open args file handle");
serde_json::from_reader(metadata_file).log_expect("Unable to read metadata file")
};
let matches = app.get_matches_from(database);
info!("Parsed command line arguments from original instance");
remove_file(args_file).log_expect("Unable to clean up args file");
Some(matches)
} else {
None
}
}
pub fn cleanup(current_path: &Path) {
// Cleanup any remaining new maintenance tool instances if they exist
if cfg!(windows) {
let updater_executable = current_path.join("maintenancetool_new.exe");
if updater_executable.exists() {
// Sleep a little bit to allow Windows to close the previous file handle
thread::sleep(time::Duration::from_millis(3000));
// Attempt it a few times because Windows can hold a lock
for i in 1..=5 {
let swap_result = remove_file(&updater_executable);
match swap_result {
Ok(_) => break,
Err(e) => {
if i < 5 {
info!("Cleanup attempt failed: {:?}, retrying in 3 seconds.", e);
thread::sleep(time::Duration::from_millis(3000));
} else {
warn!("Deleting temp binary failed after 5 attempts: {:?}", e);
}
}
}
}
}
}
}

View file

@ -7,9 +7,9 @@ use reqwest::StatusCode;
use serde_json;
use sources::types::*;
use crate::sources::types::*;
use http::build_client;
use crate::http::build_client;
pub struct GithubReleases {}
@ -41,17 +41,19 @@ impl ReleaseSource for GithubReleases {
.get(&format!(
"https://api.github.com/repos/{}/releases",
config.repo
)).header(USER_AGENT, "liftinstall (j-selby)")
))
.header(USER_AGENT, "liftinstall (j-selby)")
.send()
.map_err(|x| format!("Error while sending HTTP request: {:?}", x))?;
match response.status() {
StatusCode::OK => {}
StatusCode::FORBIDDEN => {
return Err(format!(
return Err(
"GitHub is rate limiting you. Try moving to a internet connection \
that isn't shared, and/or disabling VPNs."
));
.to_string(),
);
}
_ => {
return Err(format!("Bad status code: {:?}.", response.status()));
@ -87,20 +89,25 @@ impl ReleaseSource for GithubReleases {
let string = match asset["name"].as_str() {
Some(v) => v,
None => {
return Err("JSON payload missing information about release name".to_string())
return Err(
"JSON payload missing information about release name".to_string()
);
}
};
let url = match asset["browser_download_url"].as_str() {
Some(v) => v,
None => {
return Err("JSON payload missing information about release URL".to_string())
return Err(
"JSON payload missing information about release URL".to_string()
);
}
};
files.push(File {
name: string.to_string(),
url: url.to_string(),
requires_authorization: false,
});
}

View file

@ -6,12 +6,15 @@ pub mod types;
pub mod github;
pub mod patreon;
use self::types::ReleaseSource;
/// Returns a ReleaseSource by a name, if possible
pub fn get_by_name(name: &str) -> Option<Box<ReleaseSource>> {
pub fn get_by_name(name: &str) -> Option<Box<dyn ReleaseSource>> {
match name {
"github" => Some(Box::new(github::GithubReleases::new())),
"patreon" => Some(Box::new(patreon::PatreonReleases::new())),
_ => None,
}
}

102
src/sources/patreon.rs Normal file
View file

@ -0,0 +1,102 @@
//! patreon.rs
//!
//! Contains the yuzu-emu core API implementation of a release source.
use crate::http::build_client;
use crate::sources::types::*;
use reqwest::header::USER_AGENT;
use reqwest::StatusCode;
pub struct PatreonReleases {}
/// The configuration for this release.
#[derive(Serialize, Deserialize)]
struct PatreonConfig {
repo: String,
}
impl PatreonReleases {
pub fn new() -> Self {
PatreonReleases {}
}
}
impl ReleaseSource for PatreonReleases {
fn get_current_releases(&self, _config: &TomlValue) -> Result<Vec<Release>, String> {
let config: PatreonConfig = match _config.clone().try_into() {
Ok(v) => v,
Err(v) => return Err(format!("Failed to parse release config: {:?}", v)),
};
let mut results: Vec<Release> = Vec::new();
// Build the HTTP client up
let client = build_client()?;
let mut response = client
.get(&format!(
"https://api.yuzu-emu.org/downloads/{}/",
config.repo
))
.header(USER_AGENT, "liftinstall (j-selby)")
.send()
.map_err(|x| format!("Error while sending HTTP request: {:?}", x))?;
match response.status() {
StatusCode::OK => {}
StatusCode::FORBIDDEN => {
return Err("You are not eligible to download this release".to_string());
}
_ => {
return Err(format!("Bad status code: {:?}.", response.status()));
}
}
let body = response
.text()
.map_err(|x| format!("Failed to decode HTTP response body: {:?}", x))?;
let result: serde_json::Value = serde_json::from_str(&body)
.map_err(|x| format!("Failed to parse response: {:?}", x))?;
// Parse JSON from server
let mut files = Vec::new();
let id: u64 = match result["version"].as_u64() {
Some(v) => v,
None => return Err("JSON payload missing information about ID".to_string()),
};
let downloads = match result["files"].as_array() {
Some(v) => v,
None => return Err("JSON payload not an array".to_string()),
};
for file in downloads.iter() {
let string = match file["name"].as_str() {
Some(v) => v,
None => {
return Err("JSON payload missing information about release name".to_string());
}
};
let url = match file["url"].as_str() {
Some(v) => v,
None => {
return Err("JSON payload missing information about release URL".to_string());
}
};
files.push(File {
name: string.to_string(),
url: url.to_string(),
requires_authorization: true,
});
}
results.push(Release {
version: Version::new_number(id),
files,
});
Ok(results)
}
}

View file

@ -23,9 +23,7 @@ impl Version {
fn coarse_into_semver(&self) -> SemverVersion {
match *self {
Version::Semver(ref version) => version.to_owned(),
Version::Integer(ref version) => {
SemverVersion::from((version.to_owned(), 0 as u64, 0 as u64))
}
Version::Integer(ref version) => SemverVersion::new(version.to_owned(), 0u64, 0u64),
}
}
@ -66,6 +64,7 @@ impl Ord for Version {
pub struct File {
pub name: String,
pub url: String,
pub requires_authorization: bool,
}
impl File {}

View file

@ -0,0 +1,88 @@
//! Validates that users have correct authorization to download packages.
use crate::frontend::rest::services::authentication;
use crate::installer::InstallerFramework;
use crate::logging::LoggingErrors;
use crate::tasks::resolver::ResolvePackageTask;
use crate::tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType};
pub struct CheckAuthorizationTask {
pub name: String,
}
impl Task for CheckAuthorizationTask {
fn execute(
&mut self,
mut input: Vec<TaskParamType>,
context: &mut InstallerFramework,
_messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1);
let params = input
.pop()
.log_expect("Check Authorization Task should have input from resolver!");
let (version, file) = match params {
TaskParamType::File(v, f) => Ok((v, f)),
_ => Err("Unexpected TaskParamType in CheckAuthorization: {:?}"),
}?;
if !file.requires_authorization {
return Ok(TaskParamType::Authentication(version, file, None));
}
let username = context.database.credentials.username.clone();
let token = context.database.credentials.token.clone();
let authentication = context
.config
.clone()
.log_expect("In-memory configuration doesn't exist")
.authentication
.log_expect("No authentication configuration exists while checking authorization");
let auth_url = authentication.auth_url.clone();
let pub_key_base64 = authentication.pub_key_base64.clone();
let validation = authentication.validation.clone();
// Authorizaion is required for this package so post the username and token and get a jwt_token response
let jwt_token = match authentication::authenticate_sync(auth_url, username, token) {
Ok(jwt) => jwt,
Err(_) => return Ok(TaskParamType::Authentication(version, file, None)),
};
let claims =
match authentication::validate_token(jwt_token.clone(), pub_key_base64, validation) {
Ok(c) => c,
Err(_) => return Ok(TaskParamType::Authentication(version, file, None)),
};
// Validate that they are authorized
if !claims.roles.contains(&"vip".to_string())
&& !claims.channels.contains(&"early-access".to_string())
{
return Ok(TaskParamType::Authentication(version, file, None));
}
Ok(TaskParamType::Authentication(
version,
file,
Some(jwt_token),
))
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![TaskDependency::build(
TaskOrdering::Pre,
Box::new(ResolvePackageTask {
name: self.name.clone(),
}),
)]
}
fn name(&self) -> String {
format!("CheckAuthorizationTask (for {:?})", self.name)
}
}

View file

@ -1,20 +1,19 @@
//! Downloads a package into memory.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskOrdering;
use tasks::TaskParamType;
use crate::tasks::check_authorization::CheckAuthorizationTask;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskOrdering;
use crate::tasks::TaskParamType;
use tasks::resolver::ResolvePackageTask;
use crate::http::stream_file;
use http::stream_file;
use number_prefix::NumberPrefix::{self, Prefixed, Standalone};
use number_prefix::{decimal_prefix, Prefixed, Standalone};
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
pub struct DownloadPackageTask {
pub name: String,
@ -25,16 +24,25 @@ impl Task for DownloadPackageTask {
&mut self,
mut input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1);
let file = input.pop().log_expect("Should have input from resolver!");
let (version, file) = match file {
TaskParamType::File(v, f) => (v, f),
let file = input
.pop()
.log_expect("Download Package Task should have input from resolver!");
let (version, file, auth) = match file {
TaskParamType::Authentication(v, f, auth) => (v, f, auth),
_ => return Err("Unexpected param type to download package".to_string()),
};
// TODO: move this back below checking for latest version after testing is done
if file.requires_authorization && auth.is_none() {
info!("Authorization required to update this package!");
messenger(&TaskMessage::AuthorizationRequired("AuthorizationRequired"));
return Ok(TaskParamType::Break);
}
// Check to see if this is the newest file available already
for element in &context.database.packages {
if element.name == self.name {
@ -54,7 +62,7 @@ impl Task for DownloadPackageTask {
let mut downloaded = 0;
let mut data_storage: Vec<u8> = Vec::new();
stream_file(&file.url, |data, size| {
stream_file(&file.url, auth, |data, size| {
{
data_storage.extend_from_slice(&data);
}
@ -68,11 +76,11 @@ impl Task for DownloadPackageTask {
};
// Pretty print data volumes
let pretty_current = match decimal_prefix(downloaded as f64) {
let pretty_current = match NumberPrefix::decimal(downloaded as f64) {
Standalone(bytes) => format!("{} bytes", bytes),
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
};
let pretty_total = match decimal_prefix(size as f64) {
let pretty_total = match NumberPrefix::decimal(size as f64) {
Standalone(bytes) => format!("{} bytes", bytes),
Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix),
};
@ -92,7 +100,7 @@ impl Task for DownloadPackageTask {
fn dependencies(&self) -> Vec<TaskDependency> {
vec![TaskDependency::build(
TaskOrdering::Pre,
Box::new(ResolvePackageTask {
Box::new(CheckAuthorizationTask {
name: self.name.clone(),
}),
)]

View file

@ -1,14 +1,14 @@
//! Verifies that this is the only running instance of the installer, and that no application is running.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use native::Process;
use native::get_process_names;
use crate::native::get_process_names;
use crate::native::Process;
use std::process;
@ -19,11 +19,11 @@ impl Task for EnsureOnlyInstanceTask {
&mut self,
input: Vec<TaskParamType>,
context: &mut InstallerFramework,
_messenger: &Fn(&TaskMessage),
_messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0);
let current_pid = process::id() as usize;
let current_pid = process::id() as usize;
for Process { pid, name } in get_process_names() {
if pid == current_pid {
continue;
@ -32,13 +32,13 @@ impl Task for EnsureOnlyInstanceTask {
let exe = name;
if exe.ends_with("maintenancetool.exe") || exe.ends_with("maintenancetool") {
return Err(format!("Maintenance tool is already running!"));
return Err("Maintenance tool is already running!".to_string());
}
for package in &context.database.packages {
for file in &package.files {
if exe.ends_with(file) {
return Err(format!("The installed application is currently running!"));
return Err("The installed application is currently running!".to_string());
}
}
}
@ -52,6 +52,6 @@ impl Task for EnsureOnlyInstanceTask {
}
fn name(&self) -> String {
format!("EnsureOnlyInstanceTask")
"EnsureOnlyInstanceTask".to_string()
}
}

View file

@ -1,24 +1,29 @@
//! Overall hierarchy for installing a installation of the application.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::ensure_only_instance::EnsureOnlyInstanceTask;
use tasks::install_dir::VerifyInstallDirTask;
use tasks::install_global_shortcut::InstallGlobalShortcutsTask;
use tasks::install_pkg::InstallPackageTask;
use tasks::save_executable::SaveExecutableTask;
use tasks::uninstall_pkg::UninstallPackageTask;
use crate::tasks::ensure_only_instance::EnsureOnlyInstanceTask;
use crate::tasks::install_dir::VerifyInstallDirTask;
use crate::tasks::install_global_shortcut::InstallGlobalShortcutsTask;
use crate::tasks::install_pkg::InstallPackageTask;
use crate::tasks::launch_installed_on_exit::LaunchOnExitTask;
use crate::tasks::remove_target_dir::RemoveTargetDirTask;
use crate::tasks::save_executable::SaveExecutableTask;
use crate::tasks::uninstall_pkg::UninstallPackageTask;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskOrdering;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskOrdering;
use crate::tasks::TaskParamType;
pub struct InstallTask {
pub items: Vec<String>,
pub uninstall_items: Vec<String>,
pub fresh_install: bool,
pub create_desktop_shortcuts: bool,
// force_install: remove the target directory before installing
pub force_install: bool,
}
impl Task for InstallTask {
@ -26,7 +31,7 @@ impl Task for InstallTask {
&mut self,
_: Vec<TaskParamType>,
_: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
Ok(TaskParamType::None)
@ -40,6 +45,13 @@ impl Task for InstallTask {
Box::new(EnsureOnlyInstanceTask {}),
));
if self.force_install {
elements.push(TaskDependency::build(
TaskOrdering::Pre,
Box::new(RemoveTargetDirTask {}),
));
}
elements.push(TaskDependency::build(
TaskOrdering::Pre,
Box::new(VerifyInstallDirTask {
@ -47,13 +59,6 @@ impl Task for InstallTask {
}),
));
for item in &self.items {
elements.push(TaskDependency::build(
TaskOrdering::Pre,
Box::new(InstallPackageTask { name: item.clone() }),
));
}
for item in &self.uninstall_items {
elements.push(TaskDependency::build(
TaskOrdering::Pre,
@ -64,6 +69,16 @@ impl Task for InstallTask {
));
}
for item in &self.items {
elements.push(TaskDependency::build(
TaskOrdering::Pre,
Box::new(InstallPackageTask {
name: item.clone(),
create_desktop_shortcuts: self.create_desktop_shortcuts,
}),
));
}
if self.fresh_install {
elements.push(TaskDependency::build(
TaskOrdering::Pre,
@ -74,6 +89,11 @@ impl Task for InstallTask {
TaskOrdering::Pre,
Box::new(InstallGlobalShortcutsTask {}),
));
elements.push(TaskDependency::build(
TaskOrdering::Post,
Box::new(LaunchOnExitTask {}),
))
}
elements

View file

@ -0,0 +1,133 @@
//! Generates shortcuts for a specified file.
use crate::installer::InstallerFramework;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use crate::config::PackageDescription;
use crate::logging::LoggingErrors;
#[cfg(windows)]
use crate::native::create_desktop_shortcut;
#[cfg(target_os = "linux")]
use crate::native::create_shortcut;
pub struct InstallDesktopShortcutTask {
pub name: String,
pub should_run: bool,
}
impl Task for InstallDesktopShortcutTask {
fn execute(
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
if !self.should_run {
return Ok(TaskParamType::GeneratedShortcuts(Vec::new()));
}
messenger(&TaskMessage::DisplayMessage(
&format!(
"Generating desktop shortcuts for package {:?}...",
self.name
),
0.0,
));
let path = context
.install_path
.as_ref()
.log_expect("No install path specified");
let starting_dir = path
.to_str()
.log_expect("Unable to build shortcut metadata (startingdir)");
let mut installed_files = Vec::new();
let mut metadata: Option<PackageDescription> = None;
for description in &context
.config
.as_ref()
.log_expect("Should have packages by now")
.packages
{
if self.name == description.name {
metadata = Some(description.clone());
break;
}
}
let package = match metadata {
Some(v) => v,
None => return Err(format!("Package {:?} could not be found.", self.name)),
};
// Generate installer path
let platform_extension = if cfg!(windows) {
"maintenancetool.exe"
} else {
"maintenancetool"
};
for shortcut in package.shortcuts {
let tool_path = path.join(platform_extension);
let tool_path = tool_path
.to_str()
.log_expect("Unable to build shortcut metadata (tool)");
let exe_path = path.join(shortcut.relative_path);
let exe_path = exe_path
.to_str()
.log_expect("Unable to build shortcut metadata (exe)");
#[cfg(windows)]
installed_files.push(create_desktop_shortcut(
&shortcut.name,
&shortcut.description,
tool_path,
// TODO: Send by list
&format!("--launcher \"{}\"", exe_path),
&starting_dir,
exe_path,
)?);
#[cfg(target_os = "linux")]
installed_files.push(create_shortcut(
&shortcut.name,
&shortcut.description,
tool_path,
&format!("--launcher \"{}\"", exe_path),
&starting_dir,
exe_path,
)?);
}
// Update the installed packages shortcuts information in the database
let packages = &mut context.database.packages;
for pack in packages {
if pack.name == self.name {
pack.shortcuts.extend(installed_files.clone());
}
}
Ok(TaskParamType::GeneratedShortcuts(installed_files))
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![]
}
fn name(&self) -> String {
format!(
"InstallDesktopShortcutTask (for {:?}, should_run = {:?})",
self.name, self.should_run
)
}
}

View file

@ -1,16 +1,16 @@
//! Verifies properties about the installation directory.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use std::fs::create_dir_all;
use std::fs::read_dir;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
pub struct VerifyInstallDirTask {
pub clean_install: bool,
@ -21,7 +21,7 @@ impl Task for VerifyInstallDirTask {
&mut self,
input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0);
messenger(&TaskMessage::DisplayMessage(

View file

@ -1,17 +1,17 @@
//! Generates the global shortcut for this application.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use native::create_shortcut;
use tasks::save_database::SaveDatabaseTask;
use tasks::TaskOrdering;
use crate::native::create_shortcut;
use crate::tasks::save_database::SaveDatabaseTask;
use crate::tasks::TaskOrdering;
pub struct InstallGlobalShortcutsTask {}
@ -20,7 +20,7 @@ impl Task for InstallGlobalShortcutsTask {
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
messenger(&TaskMessage::DisplayMessage(
"Generating global shortcut...",
@ -58,9 +58,10 @@ impl Task for InstallGlobalShortcutsTask {
// TODO: Send by list
"",
&starting_dir,
"",
)?;
if !shortcut_file.is_empty() {
if !shortcut_file.is_empty() && !context.database.shortcuts.contains(&shortcut_file) {
context.database.shortcuts.push(shortcut_file);
}

View file

@ -1,32 +1,35 @@
//! Installs a specific package.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::download_pkg::DownloadPackageTask;
use tasks::install_shortcuts::InstallShortcutsTask;
use tasks::save_database::SaveDatabaseTask;
use tasks::uninstall_pkg::UninstallPackageTask;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskOrdering;
use tasks::TaskParamType;
use crate::tasks::download_pkg::DownloadPackageTask;
use crate::tasks::install_shortcuts::InstallShortcutsTask;
use crate::tasks::save_database::SaveDatabaseTask;
use crate::tasks::uninstall_pkg::UninstallPackageTask;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskOrdering;
use crate::tasks::TaskParamType;
use config::PackageDescription;
use installer::LocalInstallation;
use crate::config::PackageDescription;
use crate::installer::LocalInstallation;
use std::fs::create_dir_all;
use std::io::copy;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use archives;
use crate::archives;
use crate::tasks::install_desktop_shortcut::InstallDesktopShortcutTask;
use std::collections::HashSet;
use std::fs::OpenOptions;
use std::path::Path;
pub struct InstallPackageTask {
pub name: String,
pub create_desktop_shortcuts: bool,
}
impl Task for InstallPackageTask {
@ -34,7 +37,7 @@ impl Task for InstallPackageTask {
&mut self,
mut input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
messenger(&TaskMessage::DisplayMessage(
&format!("Installing package {:?}...", self.name),
@ -66,20 +69,20 @@ impl Task for InstallPackageTask {
None => return Err(format!("Package {:?} could not be found.", self.name)),
};
// Grab data from the shortcut generator
let shortcuts = input.pop().log_expect("Should have input from resolver!");
let shortcuts = match shortcuts {
TaskParamType::GeneratedShortcuts(files) => files,
// If the resolver returned early, we need to unwind
// Ignore input from the uninstaller - no useful information passed
// If a previous task Breaks, then just early exit
match input
.pop()
.log_expect("Install Package Task should have guaranteed output!")
{
TaskParamType::Break => return Ok(TaskParamType::None),
_ => return Err("Unexpected shortcuts param type to install package".to_string()),
_ => (),
};
// Ignore input from the uninstaller - no useful information passed
input.pop();
// Grab data from the resolver
let data = input.pop().log_expect("Should have input from resolver!");
let data = input
.pop()
.log_expect("Install Package Task should have input from resolver!");
let (version, file, data) = match data {
TaskParamType::FileContents(version, file, data) => (version, file, data),
_ => return Err("Unexpected file contents param type to install package".to_string()),
@ -139,7 +142,7 @@ impl Task for InstallPackageTask {
info!("Creating file: {:?}", string_name);
if !installed_files.contains(&string_name) {
installed_files.push(string_name.to_string());
installed_files.push(string_name);
}
let mut file_metadata = OpenOptions::new();
@ -168,9 +171,9 @@ impl Task for InstallPackageTask {
// Save metadata about this package
context.database.packages.push(LocalInstallation {
name: package.name.to_owned(),
name: package.name,
version,
shortcuts,
shortcuts: HashSet::new(),
files: installed_files,
});
@ -195,11 +198,18 @@ impl Task for InstallPackageTask {
}),
),
TaskDependency::build(
TaskOrdering::Pre,
TaskOrdering::Post,
Box::new(InstallShortcutsTask {
name: self.name.clone(),
}),
),
TaskDependency::build(
TaskOrdering::Post,
Box::new(InstallDesktopShortcutTask {
name: self.name.clone(),
should_run: self.create_desktop_shortcuts,
}),
),
TaskDependency::build(TaskOrdering::Post, Box::new(SaveDatabaseTask {})),
]
}

View file

@ -1,17 +1,17 @@
//! Generates shortcuts for a specified file.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use config::PackageDescription;
use crate::config::PackageDescription;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use native::create_shortcut;
use crate::native::create_shortcut;
pub struct InstallShortcutsTask {
pub name: String,
@ -22,7 +22,7 @@ impl Task for InstallShortcutsTask {
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
messenger(&TaskMessage::DisplayMessage(
&format!("Generating shortcuts for package {:?}...", self.name),
@ -83,9 +83,18 @@ impl Task for InstallShortcutsTask {
// TODO: Send by list
&format!("--launcher \"{}\"", exe_path),
&starting_dir,
exe_path,
)?);
}
// Update the installed packages shortcuts information in the database
let packages = &mut context.database.packages;
for pack in packages {
if pack.name == self.name {
pack.shortcuts.extend(installed_files.clone());
}
}
Ok(TaskParamType::GeneratedShortcuts(installed_files))
}

View file

@ -0,0 +1,76 @@
//! Configures lift to launch the new package on fresh install after its closed
//! If theres multiple launchable packages, then choose the first listed in config
//! If there are multiple shortcuts for the first package, then launch the first.
use crate::installer::InstallerFramework;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use crate::config::PackageDescription;
use crate::logging::LoggingErrors;
pub struct LaunchOnExitTask {}
impl Task for LaunchOnExitTask {
fn execute(
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
_: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
let pkg = &context.database.packages.first();
if pkg.is_none() {
return Ok(TaskParamType::None);
}
let pkg = pkg.unwrap();
// look up the first shortcut for the first listed package in the database
let path = context
.install_path
.as_ref()
.log_expect("No install path specified");
let mut metadata: Option<PackageDescription> = None;
for description in &context
.config
.as_ref()
.log_expect("Should have packages by now")
.packages
{
if pkg.name == description.name {
metadata = Some(description.clone());
break;
}
}
let package_desc = match metadata {
Some(v) => v,
// Package metadata is missing. Dunno what went wrong but we can skip this then
None => return Ok(TaskParamType::None),
};
let shortcut = package_desc.shortcuts.first();
// copy the path to the actual exe into launcher_path so it'll load it on exit
context.launcher_path = shortcut.map(|s| {
path.join(s.relative_path.clone())
.to_str()
.map(|t| t.to_string())
.unwrap()
});
Ok(TaskParamType::None)
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![]
}
fn name(&self) -> String {
"LaunchOnExitTask".to_string()
}
}

View file

@ -4,18 +4,22 @@
use std::fmt;
use std::fmt::Display;
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use sources::types::File;
use sources::types::Version;
use crate::sources::types::File;
use crate::sources::types::Version;
pub mod check_authorization;
pub mod download_pkg;
pub mod ensure_only_instance;
pub mod install;
pub mod install_desktop_shortcut;
pub mod install_dir;
pub mod install_global_shortcut;
pub mod install_pkg;
pub mod install_shortcuts;
pub mod launch_installed_on_exit;
pub mod remove_target_dir;
pub mod resolver;
pub mod save_database;
pub mod save_executable;
@ -29,6 +33,8 @@ pub enum TaskParamType {
None,
/// Metadata about a file
File(Version, File),
/// Authentication token for a package
Authentication(Version, File, Option<String>),
/// Downloaded contents of a file
FileContents(Version, File, Vec<u8>),
/// List of shortcuts that have been generated
@ -49,12 +55,12 @@ pub enum TaskOrdering {
/// A dependency of a task with various properties.
pub struct TaskDependency {
ordering: TaskOrdering,
task: Box<Task>,
task: Box<dyn Task>,
}
impl TaskDependency {
/// Builds a new dependency from the specified task.
pub fn build(ordering: TaskOrdering, task: Box<Task>) -> TaskDependency {
pub fn build(ordering: TaskOrdering, task: Box<dyn Task>) -> TaskDependency {
TaskDependency { ordering, task }
}
}
@ -62,6 +68,7 @@ impl TaskDependency {
/// A message from a task.
pub enum TaskMessage<'a> {
DisplayMessage(&'a str, f64),
AuthorizationRequired(&'a str),
PackageInstalled,
}
@ -74,7 +81,7 @@ pub trait Task {
&mut self,
input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String>;
/// Returns a vector containing all dependencies that need to be executed
@ -87,7 +94,7 @@ pub trait Task {
/// The dependency tree allows for smart iteration on a Task struct.
pub struct DependencyTree {
task: Box<Task>,
task: Box<dyn Task>,
dependencies: Vec<(TaskOrdering, DependencyTree)>,
}
@ -120,7 +127,7 @@ impl DependencyTree {
pub fn execute(
&mut self,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
let total_tasks = (self.dependencies.len() + 1) as f64;
@ -133,8 +140,8 @@ impl DependencyTree {
continue;
}
let result = i.execute(context, &|msg: &TaskMessage| match msg {
&TaskMessage::DisplayMessage(msg, progress) => {
let result = i.execute(context, &|msg: &TaskMessage| match *msg {
TaskMessage::DisplayMessage(msg, progress) => {
messenger(&TaskMessage::DisplayMessage(
msg,
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
@ -159,8 +166,8 @@ impl DependencyTree {
let task_result = self
.task
.execute(inputs, context, &|msg: &TaskMessage| match msg {
&TaskMessage::DisplayMessage(msg, progress) => {
.execute(inputs, context, &|msg: &TaskMessage| match *msg {
TaskMessage::DisplayMessage(msg, progress) => {
messenger(&TaskMessage::DisplayMessage(
msg,
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
@ -179,8 +186,8 @@ impl DependencyTree {
continue;
}
let result = i.execute(context, &|msg: &TaskMessage| match msg {
&TaskMessage::DisplayMessage(msg, progress) => {
let result = i.execute(context, &|msg: &TaskMessage| match *msg {
TaskMessage::DisplayMessage(msg, progress) => {
messenger(&TaskMessage::DisplayMessage(
msg,
progress / total_tasks + (1.0 / total_tasks) * f64::from(count),
@ -206,7 +213,7 @@ impl DependencyTree {
}
/// Builds a new pipeline from the specified task, iterating on dependencies.
pub fn build(task: Box<Task>) -> DependencyTree {
pub fn build(task: Box<dyn Task>) -> DependencyTree {
let dependencies = task
.dependencies()
.into_iter()

View file

@ -0,0 +1,64 @@
//! remove the whole target directory from the existence
use crate::installer::InstallerFramework;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
pub struct RemoveTargetDirTask {}
impl Task for RemoveTargetDirTask {
fn execute(
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
messenger(&TaskMessage::DisplayMessage(
"Removing previous install...",
0.1,
));
// erase the database as well
context.database.packages = Vec::new();
if let Some(path) = context.install_path.as_ref() {
let entries = std::fs::read_dir(path)
.map_err(|e| format!("Error reading {}: {}", path.to_string_lossy(), e))?;
// remove everything under the path
if !context.preexisting_install {
std::fs::remove_dir_all(path)
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
return Ok(TaskParamType::None);
}
// remove everything except the maintenancetool if repairing
for entry in entries {
let path = entry
.map_err(|e| format!("Error reading file: {}", e))?
.path();
if let Some(filename) = path.file_name() {
if filename.to_string_lossy().starts_with("maintenancetool") {
continue;
}
}
if path.is_dir() {
std::fs::remove_dir_all(&path)
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
} else {
std::fs::remove_file(&path)
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
}
}
}
Ok(TaskParamType::None)
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![]
}
fn name(&self) -> String {
"RemoveTargetDirTask".to_string()
}
}

View file

@ -2,18 +2,18 @@
use std::env::consts::OS;
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use config::PackageDescription;
use crate::config::PackageDescription;
use regex::Regex;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
pub struct ResolvePackageTask {
pub name: String,
@ -24,7 +24,7 @@ impl Task for ResolvePackageTask {
&mut self,
input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0);
let mut metadata: Option<PackageDescription> = None;

View file

@ -1,11 +1,11 @@
//! Saves the main database into the installation directory.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
pub struct SaveDatabaseTask {}
@ -14,7 +14,7 @@ impl Task for SaveDatabaseTask {
&mut self,
input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0);
messenger(&TaskMessage::DisplayMessage(

View file

@ -1,11 +1,11 @@
//! Saves the installer executable into the install directory.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use std::fs::File;
use std::fs::OpenOptions;
@ -14,7 +14,7 @@ use std::io::copy;
use std::env::current_exe;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
pub struct SaveExecutableTask {}
@ -23,7 +23,7 @@ impl Task for SaveExecutableTask {
&mut self,
input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0);
messenger(&TaskMessage::DisplayMessage(

View file

@ -1,14 +1,14 @@
//! Uninstalls a set of packages.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskParamType;
use tasks::uninstall_pkg::UninstallPackageTask;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskOrdering;
use crate::tasks::uninstall_pkg::UninstallPackageTask;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskOrdering;
pub struct UninstallTask {
pub items: Vec<String>,
@ -19,7 +19,7 @@ impl Task for UninstallTask {
&mut self,
_: Vec<TaskParamType>,
_: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
messenger(&TaskMessage::DisplayMessage("Wrapping up...", 0.0));
Ok(TaskParamType::None)

View file

@ -1,15 +1,15 @@
//! Uninstalls a specific package.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use crate::tasks::save_database::SaveDatabaseTask;
use crate::tasks::TaskOrdering;
use std::fs::remove_file;
use tasks::save_database::SaveDatabaseTask;
use tasks::TaskOrdering;
pub struct UninstallGlobalShortcutsTask {}
@ -18,7 +18,7 @@ impl Task for UninstallGlobalShortcutsTask {
&mut self,
input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0);

View file

@ -1,21 +1,21 @@
//! Uninstalls a specific package.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::save_database::SaveDatabaseTask;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskOrdering;
use tasks::TaskParamType;
use crate::tasks::save_database::SaveDatabaseTask;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskOrdering;
use crate::tasks::TaskParamType;
use installer::LocalInstallation;
use crate::installer::LocalInstallation;
use std::fs::remove_dir;
use std::fs::remove_file;
use logging::LoggingErrors;
use tasks::uninstall_shortcuts::UninstallShortcutsTask;
use crate::logging::LoggingErrors;
use crate::tasks::uninstall_shortcuts::UninstallShortcutsTask;
pub struct UninstallPackageTask {
pub name: String,
@ -27,7 +27,7 @@ impl Task for UninstallPackageTask {
&mut self,
input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1);
@ -44,7 +44,7 @@ impl Task for UninstallPackageTask {
}
}
let mut package = match metadata {
let package = match metadata {
Some(v) => v,
None => {
if self.optional {
@ -63,8 +63,7 @@ impl Task for UninstallPackageTask {
0.0,
));
// Reverse, as to delete directories last
package.files.reverse();
let mut directories = Vec::new();
let max = package.files.len();
for (i, file) in package.files.iter().enumerate() {
@ -78,7 +77,9 @@ impl Task for UninstallPackageTask {
));
let result = if file.is_dir() {
remove_dir(file)
// we don't delete directory just yet
directories.push(file);
Ok(())
} else {
remove_file(file)
};
@ -88,6 +89,17 @@ impl Task for UninstallPackageTask {
}
}
// sort directories by reverse depth order
directories.sort_by(|a, b| {
let depth_a = a.components().fold(0usize, |acc, _| acc + 1);
let depth_b = b.components().fold(0usize, |acc, _| acc + 1);
depth_b.cmp(&depth_a)
});
for i in directories.iter() {
info!("Deleting directory: {:?}", i);
remove_dir(i).ok();
}
Ok(TaskParamType::None)
}

View file

@ -1,18 +1,18 @@
//! Uninstalls a specific package.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use installer::LocalInstallation;
use crate::installer::LocalInstallation;
use std::fs::remove_dir;
use std::fs::remove_file;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
pub struct UninstallShortcutsTask {
pub name: String,
@ -24,7 +24,7 @@ impl Task for UninstallShortcutsTask {
&mut self,
input: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &Fn(&TaskMessage),
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 0);

File diff suppressed because one or more lines are too long

View file

@ -1,88 +0,0 @@
/* roboto-regular - latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url('../fonts/roboto-v18-latin-regular.eot'); /* IE9 Compat Modes */
src: local('Roboto'), local('Roboto-Regular'),
url('../fonts/roboto-v18-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('../fonts/roboto-v18-latin-regular.woff') format('woff');
}
html, body {
overflow: hidden;
height: 100%;
}
body, div, span, h1, h2, h3, h4, h5, h6 {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: default;
}
pre {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
cursor: text;
}
.tile.is-child > .box {
height: 100%;
}
.has-padding {
padding: 2rem;
position: relative;
}
.clickable-box {
cursor: pointer;
}
.clickable-box label {
pointer-events: none;
}
.is-max-height {
height: 100%;
}
.is-bottom-floating {
position: absolute;
bottom: 0;
}
.is-right-floating {
position: absolute;
right: 0;
}
.has-padding .is-right-floating {
right: 1rem;
}
.is-left-floating {
position: absolute;
left: 0;
}
.has-padding .is-left-floating {
left: 1rem;
}
.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
padding: 20px;
background: #fff;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,67 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title id="window-title">... Installer</title>
<link rel="icon" href="/favicon.ico" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/css/bulma.min.css" type="text/css">
<link rel="stylesheet" href="/css/main.css" type="text/css">
</head>
<body class="is-max-height">
<div class="fullscreen" id="ie-blackout" style="display: none">
<div class="title">Your computer is out of date.</div>
<div class="subtitle">
Make sure that your computer is up to date, and that you have Internet Explorer 11 installed.
</div>
<div class="subtitle">
Please note we do not support pirated or unsupported versions of Windows.
</div>
</div>
<script type="text/javascript">
if (!document.__proto__) {
document.getElementById("ie-blackout").style.display = "block";
}
</script>
<div id="app" class="is-max-height">
<section class="section is-max-height">
<div class="container is-max-height">
<div class="columns is-max-height">
<div class="column is-one-third has-padding" v-if="!metadata.is_launcher">
<img src="/logo.png" width="60%" />
<br />
<br />
<h2 class="subtitle" v-if="!metadata.preexisting_install">
Welcome to the {{ attrs.name }} installer!
</h2>
<h2 class="subtitle" v-if="!metadata.preexisting_install">
We will have you up and running in just a few moments.
</h2>
<h2 class="subtitle" v-if="metadata.preexisting_install">
Welcome to the {{ attrs.name }} Maintenance Tool.
</h2>
</div>
<router-view></router-view>
</div>
</div>
</section>
</div>
<script src="/js/vue.min.js" type="text/javascript"></script>
<script src="/js/vue-router.min.js" type="text/javascript"></script>
<script src="/api/attrs" type="text/javascript"></script>
<script src="/js/helpers.js" type="text/javascript"></script>
<script src="/js/views.js" type="text/javascript"></script>
<script src="/js/main.js" type="text/javascript"></script>
</body>
</html>

View file

@ -1,144 +0,0 @@
/**
* helpers.js
*
* Additional state-less helper methods.
*/
var request_id = 0;
/**
* Makes a AJAX request.
*
* @param path The path to connect to.
* @param successCallback A callback with a JSON payload.
* @param failCallback A fail callback. Optional.
* @param data POST data. Optional.
*/
function ajax(path, successCallback, failCallback, data) {
if (failCallback === undefined) {
failCallback = defaultFailHandler;
}
console.log("Making HTTP request to " + path);
var req = new XMLHttpRequest();
req.addEventListener("load", function() {
// The server can sometimes return a string error. Make sure we handle this.
if (this.status === 200 && this.getResponseHeader('Content-Type').indexOf("application/json") !== -1) {
successCallback(JSON.parse(this.responseText));
} else {
failCallback(this.responseText);
}
});
req.addEventListener("error", failCallback);
req.open(data == null ? "GET" : "POST", path + "?nocache=" + request_id++, true);
// Rocket only currently supports URL encoded forms.
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
if (data != null) {
var form = "";
for (var key in data) {
if (!data.hasOwnProperty(key)) {
continue;
}
if (form !== "") {
form += "&";
}
form += encodeURIComponent(key) + "=" + encodeURIComponent(data[key]);
}
req.send(form);
} else {
req.send();
}
}
/**
* Makes a AJAX request, streaming each line as it arrives. Type should be text/plain,
* each line will be interpreted as JSON separately.
*
* @param path The path to connect to.
* @param callback A callback with a JSON payload. Called for every line as it comes.
* @param successCallback A callback with a raw text payload.
* @param failCallback A fail callback. Optional.
* @param data POST data. Optional.
*/
function stream_ajax(path, callback, successCallback, failCallback, data) {
var req = new XMLHttpRequest();
console.log("Making streaming HTTP request to " + path);
req.addEventListener("load", function() {
// The server can sometimes return a string error. Make sure we handle this.
if (this.status === 200) {
successCallback(this.responseText);
} else {
failCallback(this.responseText);
}
});
var buffer = "";
var seenBytes = 0;
req.onreadystatechange = function() {
if(req.readyState > 2) {
buffer += req.responseText.substr(seenBytes);
var pointer;
while ((pointer = buffer.indexOf("\n")) >= 0) {
var line = buffer.substring(0, pointer).trim();
buffer = buffer.substring(pointer + 1);
if (line.length === 0) {
continue;
}
var contents = JSON.parse(line);
callback(contents);
}
seenBytes = req.responseText.length;
}
};
req.addEventListener("error", failCallback);
req.open(data == null ? "GET" : "POST", path + "?nocache=" + request_id++, true);
// Rocket only currently supports URL encoded forms.
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
if (data != null) {
var form = "";
for (var key in data) {
if (!data.hasOwnProperty(key)) {
continue;
}
if (form !== "") {
form += "&";
}
form += encodeURIComponent(key) + "=" + encodeURIComponent(data[key]);
}
req.send(form);
} else {
req.send();
}
}
/**
* The default handler if a AJAX request fails. Not to be used directly.
*
* @param e The XMLHttpRequest that failed.
*/
function defaultFailHandler(e) {
console.error("A AJAX request failed, and was not caught:");
console.error(e);
}

View file

@ -1,68 +0,0 @@
// Overwrite loggers with the logging backend
if (window.external !== undefined && window.external.invoke !== undefined) {
window.onerror = function(msg, url, line) {
old_onerror(msg, url, line);
window.external.invoke(JSON.stringify({
Log: {
kind: "error",
msg: msg + " @ " + url + ":" + line
}
}));
};
// Borrowed from http://tobyho.com/2012/07/27/taking-over-console-log/
function intercept(method){
console[method] = function(){
var message = Array.prototype.slice.apply(arguments).join(' ');
window.external.invoke(JSON.stringify({
Log: {
kind: method,
msg: message
}
}));
}
}
var methods = ['log', 'warn', 'error'];
for (var i = 0; i < methods.length; i++) {
intercept(methods[i]);
}
}
// Disable F5
function disable_shortcuts(e) {
switch (e.keyCode) {
case 116: // F5
e.preventDefault();
break;
}
}
window.addEventListener("keydown", disable_shortcuts);
document.getElementById("window-title").innerText = base_attributes.name + " Installer";
function selectFileCallback(name) {
app.install_location = name;
}
var app = new Vue({
router: router,
data: {
attrs: base_attributes,
config : {},
install_location : "",
// If the option to pick an install location should be provided
show_install_location : true,
metadata : {
database : [],
install_path : "",
preexisting_install : false
}
},
methods: {
"exit": function() {
ajax("/api/exit", function() {});
}
}
}).$mount("#app");

View file

@ -1,460 +0,0 @@
const DownloadConfig = {
template: `
<div class="column has-padding">
<h4 class="subtitle">Downloading config...</h4>
<br />
<progress class="progress is-info is-medium" value="0" max="100">
0%
</progress>
</div>
`,
created: function() {
this.download_install_status();
},
methods: {
download_install_status: function() {
var that = this; // IE workaround
ajax("/api/installation-status", function(e) {
app.metadata = e;
that.download_config();
});
},
download_config: function() {
var that = this; // IE workaround
ajax("/api/config", function(e) {
app.config = e;
that.choose_next_state();
}, function(e) {
console.error("Got error while downloading config: "
+ e);
if (app.metadata.is_launcher) {
// Just launch the target application
app.exit();
} else {
router.replace({name: 'showerr', params: {msg: "Got error while downloading config: "
+ e}});
}
});
},
choose_next_state: function() {
// Update the updater if needed
if (app.config.new_tool) {
router.push("/install/updater");
return;
}
if (app.metadata.preexisting_install) {
app.install_location = app.metadata.install_path;
// Copy over installed packages
for (var x = 0; x < app.config.packages.length; x++) {
app.config.packages[x].default = false;
app.config.packages[x].installed = false;
}
for (var i = 0; i < app.metadata.database.packages.length; i++) {
// Find this config package
for (var x = 0; x < app.config.packages.length; x++) {
if (app.config.packages[x].name === app.metadata.database.packages[i].name) {
app.config.packages[x].default = true;
app.config.packages[x].installed = true;
}
}
}
if (app.metadata.is_launcher) {
router.replace("/install/regular");
} else {
router.replace("/modify");
}
} else {
for (var x = 0; x < app.config.packages.length; x++) {
app.config.packages[x].installed = false;
}
// Need to do a bit more digging to get at the
// install location.
ajax("/api/default-path", function(e) {
if (e.path != null) {
app.install_location = e.path;
}
});
router.replace("/packages");
}
}
}
};
const SelectPackages = {
template: `
<div class="column has-padding">
<h4 class="subtitle">Select which packages you want to install:</h4>
<!-- Build options -->
<div class="tile is-ancestor">
<div class="tile is-parent" v-for="package in $root.$data.config.packages" :index="package.name">
<div class="tile is-child">
<div class="box clickable-box" v-on:click.capture.stop="package.default = !package.default">
<label class="checkbox">
<input type="checkbox" v-model="package.default" />
{{ package.name }}
<span v-if="package.installed"><i>(installed)</i></span>
</label>
<p>
{{ package.description }}
</p>
</div>
</div>
</div>
</div>
<div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">Install Location</div>
<div class="field has-addons" v-if="!$root.$data.metadata.preexisting_install && advanced">
<div class="control is-expanded">
<input class="input" type="text" v-model="$root.$data.install_location"
placeholder="Enter a install path here">
</div>
<div class="control">
<a class="button is-dark" v-on:click="select_file">
Select
</a>
</div>
</div>
<div class="is-right-floating is-bottom-floating">
<div class="field is-grouped">
<p class="control">
<a class="button is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced"
v-on:click="advanced = true">Advanced...</a>
</p>
<p class="control">
<a class="button is-dark is-medium" v-if="!$root.$data.metadata.preexisting_install"
v-on:click="install">Install</a>
</p>
<p class="control">
<a class="button is-dark is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="install">Modify</a>
</p>
</div>
</div>
<div class="field is-grouped is-left-floating is-bottom-floating">
<p class="control">
<a class="button is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="go_back">Back</a>
</p>
</div>
</div>
`,
data: function() {
return {
advanced: false
}
},
methods: {
select_file: function() {
window.external.invoke(JSON.stringify({
SelectInstallDir: {
callback_name: "selectFileCallback"
}
}));
},
install: function() {
router.push("/install/regular");
},
go_back: function() {
router.go(-1);
}
}
};
const InstallPackages = {
template: `
<div class="column has-padding">
<h4 class="subtitle" v-if="$root.$data.metadata.is_launcher || is_update">Checking for updates...</h4>
<h4 class="subtitle" v-else-if="is_uninstall">Uninstalling...</h4>
<h4 class="subtitle" v-else-if="is_updater_update">Downloading self-update...</h4>
<h4 class="subtitle" v-else>Installing...</h4>
<div v-html="$root.$data.config.installing_message"></div>
<br />
<div v-html="progress_message"></div>
<progress class="progress is-info is-medium" v-bind:value="progress" max="100">
{{ progress }}%
</progress>
</div>
`,
data: function() {
return {
progress: 0.0,
progress_message: "Please wait...",
is_uninstall: false,
is_updater_update: false,
is_update: false,
failed_with_error: false,
packages_installed: 0
}
},
created: function() {
this.is_uninstall = this.$route.params.kind === "uninstall";
this.is_updater_update = this.$route.params.kind === "updater";
this.is_update = this.$route.params.kind === "update";
console.log("Installer kind: " + this.$route.params.kind);
this.install();
},
methods: {
install: function() {
var results = {};
for (var package_index = 0; package_index < app.config.packages.length; package_index++) {
var current_package = app.config.packages[package_index];
if (current_package.default != null) {
results[current_package.name] = current_package.default;
}
}
results["path"] = app.install_location;
var that = this; // IE workaround
var targetUrl = "/api/start-install";
if (this.is_uninstall) {
targetUrl = "/api/uninstall";
}
if (this.is_updater_update) {
targetUrl = "/api/update-updater";
}
stream_ajax(targetUrl, function(line) {
if (line.hasOwnProperty("Status")) {
that.progress_message = line.Status[0];
that.progress = line.Status[1] * 100;
}
if (line.hasOwnProperty("PackageInstalled")) {
that.packages_installed += 1;
}
if (line.hasOwnProperty("Error")) {
if (app.metadata.is_launcher) {
app.exit();
} else {
that.failed_with_error = true;
router.replace({name: 'showerr', params: {msg: line.Error}});
}
}
}, function(e) {
if (that.is_updater_update) {
// Continue with what we were doing
if (app.metadata.is_launcher) {
router.replace("/install/regular");
} else {
if (app.metadata.preexisting_install) {
router.replace("/modify");
} else {
router.replace("/packages");
}
}
} else {
if (app.metadata.is_launcher) {
app.exit();
} else if (!that.failed_with_error) {
if (that.is_uninstall) {
router.replace({name: 'complete', params: {
uninstall: true,
update: that.is_update,
installed: that.packages_installed
}});
} else {
router.replace({name: 'complete', params: {
uninstall: false,
update: that.is_update,
installed: that.packages_installed
}});
}
}
}
}, undefined, results);
}
}
};
const ErrorView = {
template: `
<div class="column has-padding">
<h4 class="subtitle">An error occurred:</h4>
<pre>{{ msg }}</pre>
<div class="field is-grouped is-right-floating is-bottom-floating">
<p class="control">
<a class="button is-primary is-medium" v-if="remaining" v-on:click="go_back">Back</a>
</p>
</div>
</div>
`,
data: function() {
return {
msg: this.$route.params.msg,
remaining: window.history.length > 1
}
},
methods: {
go_back: function() {
router.go(-1);
}
}
};
const CompleteView = {
template: `
<div class="column has-padding">
<div v-if="was_update">
<div v-if="has_installed">
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been updated.</h4>
<p>You can find your installed applications in your start menu.</p>
</div>
<div v-else>
<h4 class="subtitle">{{ $root.$data.attrs.name }} is already up to date!</h4>
<p>You can find your installed applications in your start menu.</p>
</div>
</div>
<div v-else-if="was_install">
<h4 class="subtitle">Thanks for installing {{ $root.$data.attrs.name }}!</h4>
<p>You can find your installed applications in your start menu.</p>
<img src="/how-to-open.png" />
</div>
<div v-else>
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been uninstalled.</h4>
</div>
<div class="field is-grouped is-right-floating is-bottom-floating">
<p class="control">
<a class="button is-dark is-medium" v-on:click="exit">Exit</a>
</p>
</div>
</div>
`,
data: function() {
return {
was_install: !this.$route.params.uninstall,
was_update: this.$route.params.update,
has_installed: this.$route.params.packages_installed > 0
}
},
methods: {
exit: function() {
app.exit();
}
}
};
const ModifyView = {
template: `
<div class="column has-padding">
<h4 class="subtitle">Choose an option:</h4>
<a class="button is-dark is-medium" v-on:click="update">
Update
</a>
<br />
<br />
<a class="button is-dark is-medium" v-on:click="modify_packages">
Modify
</a>
<br />
<br />
<a class="button is-dark is-medium" v-on:click="prepare_uninstall">
Uninstall
</a>
<div class="modal is-active" v-if="show_uninstall">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Are you sure you want to uninstall {{ $root.$data.attrs.name }}?</p>
</header>
<footer class="modal-card-foot">
<button class="button is-danger" v-on:click="uninstall">Yes</button>
<button class="button" v-on:click="cancel_uninstall">No</button>
</footer>
</div>
</div>
</div>
`,
data: function() {
return {
show_uninstall: false
}
},
methods: {
update: function() {
router.push("/install/update");
},
modify_packages: function() {
router.push("/packages");
},
prepare_uninstall: function() {
this.show_uninstall = true;
},
cancel_uninstall: function() {
this.show_uninstall = false;
},
uninstall: function() {
router.push("/install/uninstall");
},
}
};
const router = new VueRouter({
routes: [
{
path: '/config',
name: 'config',
component: DownloadConfig
},
{
path: '/packages',
name: 'packages',
component: SelectPackages
},
{
path: '/install/:kind',
name: 'install',
component: InstallPackages
},
{
path: '/showerr',
name: 'showerr',
component: ErrorView
},
{
path: '/complete/:uninstall/:update/:packages_installed',
name: 'complete',
component: CompleteView
},
{
path: '/modify',
name: 'modify',
component: ModifyView
},
{
path: '/',
redirect: '/config'
}
]
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
ui/.browserslistrc Normal file
View file

@ -0,0 +1 @@
> 1%

5
ui/.editorconfig Normal file
View file

@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

16
ui/.eslintrc.js Normal file
View file

@ -0,0 +1,16 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/recommended',
'@vue/standard'
],
rules: {
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-redeclare': 'off',
camelcase: 'off'
}
}

21
ui/.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

29
ui/README.md Normal file
View file

@ -0,0 +1,29 @@
# ui
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn run serve
```
### Compiles and minifies for production
```
yarn run build
```
### Run your tests
```
yarn run test
```
### Lints and fixes files
```
yarn run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
ui/babel.config.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

20
ui/merge-strings.js Executable file
View file

@ -0,0 +1,20 @@
#!/bin/env node
const fs = require('fs')
const merge = require('deepmerge')
const glob = require('glob')
glob('src/locales/!(messages).json', {}, (e, files) => {
let messages = []
for (const file of files) {
console.log(`Loading ${file}...`)
const locale_messages = require(`./${file}`)
messages.push(locale_messages)
}
console.log('Merging messages...')
if (messages && messages.length > 1) {
messages = merge.all(messages)
} else {
messages = messages[0] // single locale mode
}
fs.writeFileSync('src/locales/messages.json', JSON.stringify(messages), {})
})

173
ui/mock-server.js Normal file
View file

@ -0,0 +1,173 @@
'use strict'
const express = require('express')
const app = express()
const port = 3000
let showError = false
let showConfigError = false
let maintenance = false
let launcher = false
let fileExists = false
let darkMode = false
let recoveryMode = false
function progressSimulation (res) {
if (showError) {
const resp = JSON.stringify({ Error: 'Simulated error.' }) + '\n'
res.write(resp)
res.status(200).end()
return
}
let progress = 0.0
const timer = setInterval(() => {
const resp = JSON.stringify({ Status: ['Processing...', progress] }) + '\n'
progress += 0.1
res.write(resp)
if (progress >= 1) {
res.status(200).end()
clearInterval(timer)
}
}, 500)
}
function returnConfig (res) {
if (showConfigError) {
res.status(500).json({})
return
}
res.json({
installing_message:
'Test Banner <strong>Bold</strong>&nbsp;<pre>Code block</pre>&nbsp;<i>Italic</i>&nbsp;<del>Strike</del>',
new_tool: null,
packages: [
{
name: 'Test 1',
description: 'LiftInstall GUI Test 1',
default: true,
source: {
name: 'github',
match: '^test$',
config: { repo: 'j-selby/liftinstall' }
},
shortcuts: []
},
{
name: 'Test 2',
description:
'Different Banner <strong>Bold</strong>&nbsp;<pre>Code block</pre>&nbsp;<i>Italic</i>&nbsp;<del>Strike</del>',
default: null,
source: {
name: 'github',
match: '^test2$',
config: { repo: 'j-selby/liftinstall' }
},
shortcuts: []
}
],
hide_advanced: false
})
}
app.get('/api/attrs', (req, res) => {
console.log('-- Get attrs')
res.send(
{ name: 'yuzu', recovery: recoveryMode, target_url: 'https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml' }
)
})
app.get('/api/dark-mode', (req, res) => {
res.json(darkMode)
})
app.get('/api/installation-status', (req, res) => {
res.json({
database: { packages: [], shortcuts: [] },
install_path: null,
preexisting_install: maintenance,
is_launcher: launcher,
launcher_path: null
})
})
app.get('/api/default-path', (req, res) => {
res.json({ path: '/tmp/test/' })
})
app.get('/api/config', (req, res) => {
setTimeout(() => {
returnConfig(res)
}, 3000)
})
app.post('/api/start-install', (req, res) => {
console.log('-- Install:')
console.log(req.body)
progressSimulation(res)
})
app.get('/api/exit', (req, res) => {
console.log('-- Exit')
if (showError) {
res.status(500).send('Simulated error: Nothing to see here.')
return
}
res.status(204).send()
})
app.post('/api/verify-path', (req, res) => {
console.log('-- Verify Path')
res.send({
exists: fileExists
})
})
app.post('/api/check-auth', (req, res) => {
console.log('-- Check Authorization')
res.send({
username: 'test1',
token: 'token',
jwt_token: {
isPatreonAccountLinked: true,
isPatreonSubscriptionActive: true,
releaseChannels: ['early-access']
}
})
})
process.argv.forEach((val, index) => {
switch (val) {
case 'maintenance':
maintenance = true
console.log('Simulating maintenance mode')
break
case 'launcher':
maintenance = true
launcher = true
console.log('Simulating launcher mode')
break
case 'exists':
fileExists = true
console.log('Simulating file exists situation')
break
case 'dark':
darkMode = true
console.log('Simulating dark mode')
break
case 'config-error':
showConfigError = true
console.log('Simulating configuration errors')
break
case 'error':
showError = true
console.log('Simulating errors')
break
case 'recovery':
recoveryMode = true
console.log('Simulating recovery mode')
break
}
})
console.log(`Listening on ${port}...`)
app.listen(port)

36
ui/package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "ui",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"postinstall": "node merge-strings.js"
},
"dependencies": {
"@mdi/font": "^7.1.96",
"axios": "^1.2.1",
"buefy": "^0.9.22",
"canvas-confetti": "^1.6.0",
"vue": "^2.6.14",
"vue-axios": "^3.5.2",
"vue-i18n": "^8.26.5",
"vue-router": "^3.5.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"@vue/eslint-config-standard": "^6.1.0",
"eslint": "^8.30.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^9.8.0",
"express": "^4.18.2",
"http-proxy-middleware": "^2.0.6",
"vue-template-compiler": "^2.7.14"
}
}

5
ui/postcss.config.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

34
ui/public/index.html Normal file
View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=11">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title id="window-title">... Installer</title>
</head>
<body>
<div class="fullscreen" id="ie-blackout" style="display: none">
<div class="title">Your computer is out of date.</div>
<div class="subtitle">
Make sure that your computer is up to date, and that you have Internet Explorer 11 installed.
</div>
<div class="subtitle">
Please note we do not support pirated or unsupported versions of Windows.
</div>
</div>
<script type="text/javascript">
if (!document.__proto__) {
document.getElementById("ie-blackout").style.display = "block";
}
</script>
<noscript>
<strong>You need JavaScript enabled in your Windows Internet Options to install this application.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

240
ui/src/App.vue Normal file
View file

@ -0,0 +1,240 @@
<template>
<div id="app" class="is-max-height">
<section class="section is-max-height">
<div class="container is-max-height">
<div class="columns is-max-height">
<div class="column is-one-third has-padding" v-if="!$root.$data.metadata.is_launcher">
<img src="./assets/light_mode_installer_logo.png" id="applicationIcon" alt="Application icon" />
<br />
<br />
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install">
{{ $t('app.installer_title', {'name': $root.$data.attrs.name}) }}
</h2>
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install">
{{ $t('app.installer_subtitle') }}
</h2>
<h2 class="subtitle" v-if="$root.$data.metadata.preexisting_install">
{{ $t('app.maintenance_title', {'name': $root.$data.attrs.name}) }}
</h2>
<b-dropdown hoverable @change="selectLocale" aria-role="list" scrollable>
<button class="button" slot="trigger">
<span>{{ $t('locale') }}</span>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item v-for="(locale, index) in this.$i18n.messages" v-bind:key="index" :value="index" aria-role="listitem">{{locale.locale}}</b-dropdown-item>
</b-dropdown>
</div>
<router-view />
</div>
</div>
</section>
</div>
</template>
<script>
export default {
mounted: function () {
// detect languages
const languages = window.navigator.languages
if (languages) {
// standard-compliant browsers
for (let index = 0; index < languages.length; index++) {
const lang = languages[index]
// Find the most preferred language that we support
if (Object.prototype.hasOwnProperty.call(this.$i18n.messages, lang)) {
this.$i18n.locale = lang
return
}
}
}
// IE9+ support
this.$i18n.locale = window.navigator.browserLanguage
},
methods: {
selectLocale: function (locale) {
this.$i18n.locale = locale
}
}
}
</script>
<style>
/* roboto-regular - latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url('./assets/fonts/roboto-v18-latin-regular.eot'); /* IE9 Compat Modes */
src: local('Roboto'), local('Roboto-Regular'),
url('./assets/fonts/roboto-v18-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('./assets/fonts/roboto-v18-latin-regular.woff') format('woff');
}
html, body {
overflow: hidden;
height: 100%;
}
body, div, span, h1, h2, h3, h4, h5, h6 {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: default;
}
#applicationIcon {
width:0px; height: 0px;
padding: 50px 60% 0px 0px;
background: url("./assets/light_mode_installer_logo.png") left top no-repeat;
background-size: contain;
}
body.has-background-black-ter #applicationIcon {
background: url("./assets/dark_mode_installer_logo.png") left top no-repeat;
background-size: contain;
}
.package-icon {
width: 3rem;
height: 3rem;
float: left;
padding-right: 10px;
padding-top: 10px;
}
.package-description {
overflow: hidden;
}
pre {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
cursor: text;
}
.tile.is-child > .box {
height: 100%;
}
.has-padding {
padding: 2rem;
position: relative;
}
.clickable-box {
cursor: pointer;
position: relative;
}
.clickable-box label {
pointer-events: none;
}
.is-max-height {
height: 100%;
}
.is-bottom-floating {
position: absolute;
bottom: 0;
}
.is-top-floating {
position: absolute;
top: 0;
}
.is-right-floating {
position: absolute;
right: 0;
}
.has-padding .is-right-floating {
right: 1rem;
}
.is-left-floating {
position: absolute;
left: 0;
}
.has-padding .is-left-floating {
left: 1rem;
}
.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
padding: 20px;
background: #fff;
}
.tile.box.clickable-box {
color: #4a4a4a;
}
/* Dark mode */
body.has-background-black-ter .subtitle, body.has-background-black-ter .column > div {
color: hsl(0, 0%, 96%);
}
.ribbon {
position: absolute;
right: -5px; top: -5px;
z-index: 1;
overflow: hidden;
width: 75px; height: 75px;
text-align: right;
}
.ribbon span {
font-size: 10px;
font-weight: bold;
color: #FFF;
text-transform: uppercase;
text-align: center;
line-height: 20px;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
width: 100px;
display: block;
background: #79A70A;
background: linear-gradient(#FF3C28 0%, #FF3C28 100%);
box-shadow: 0 3px 10px -5px rgba(0, 0, 0, 1);
position: absolute;
top: 19px; right: -21px;
}
.ribbon span::before {
content: "";
position: absolute; left: 0px; top: 100%;
z-index: -1;
border-left: 3px solid #FF3C28;
border-right: 3px solid transparent;
border-bottom: 3px solid transparent;
border-top: 3px solid #FF3C28;
}
.ribbon span::after {
content: "";
position: absolute; right: 0px; top: 100%;
z-index: -1;
border-left: 3px solid transparent;
border-right: 3px solid #FF3C28;
border-bottom: 3px solid transparent;
border-top: 3px solid #FF3C28;
}
a:hover {
text-decoration: underline;
}
</style>

Some files were not shown because too many files have changed in this diff Show more