mirror of
https://github.com/cyberarm/w3d_hub_linux_launcher.git
synced 2026-03-22 12:16:15 +00:00
Compare commits
5 Commits
f651143937
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f024109327 | |||
| 287022f2b8 | |||
| 68df923bea | |||
| ddbec8d72c | |||
| 70d4e0c40f |
14
Gemfile.lock
14
Gemfile.lock
@@ -1,13 +1,13 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
async (2.36.0)
|
async (2.35.2)
|
||||||
console (~> 1.29)
|
console (~> 1.29)
|
||||||
fiber-annotation
|
fiber-annotation
|
||||||
io-event (~> 1.11)
|
io-event (~> 1.11)
|
||||||
metrics (~> 0.12)
|
metrics (~> 0.12)
|
||||||
traces (~> 0.18)
|
traces (~> 0.18)
|
||||||
async-http (0.94.2)
|
async-http (0.94.0)
|
||||||
async (>= 2.10.2)
|
async (>= 2.10.2)
|
||||||
async-pool (~> 0.11)
|
async-pool (~> 0.11)
|
||||||
io-endpoint (~> 0.14)
|
io-endpoint (~> 0.14)
|
||||||
@@ -30,7 +30,7 @@ GEM
|
|||||||
fiber-annotation
|
fiber-annotation
|
||||||
fiber-local (~> 1.1)
|
fiber-local (~> 1.1)
|
||||||
json
|
json
|
||||||
cyberarm_engine (0.25.1)
|
cyberarm_engine (0.25.0)
|
||||||
gosu (~> 1.1)
|
gosu (~> 1.1)
|
||||||
digest-crc (0.7.0)
|
digest-crc (0.7.0)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
@@ -43,7 +43,7 @@ GEM
|
|||||||
fiber-storage (1.0.1)
|
fiber-storage (1.0.1)
|
||||||
fiddle (1.1.8)
|
fiddle (1.1.8)
|
||||||
gosu (1.4.6)
|
gosu (1.4.6)
|
||||||
io-endpoint (0.17.2)
|
io-endpoint (0.16.0)
|
||||||
io-event (1.14.2)
|
io-event (1.14.2)
|
||||||
io-stream (0.11.1)
|
io-stream (0.11.1)
|
||||||
ircparser (1.0.0)
|
ircparser (1.0.0)
|
||||||
@@ -52,8 +52,8 @@ GEM
|
|||||||
fiddle
|
fiddle
|
||||||
metrics (0.15.0)
|
metrics (0.15.0)
|
||||||
protocol-hpack (1.5.1)
|
protocol-hpack (1.5.1)
|
||||||
protocol-http (0.58.1)
|
protocol-http (0.58.0)
|
||||||
protocol-http1 (0.37.0)
|
protocol-http1 (0.36.0)
|
||||||
protocol-http (~> 0.58)
|
protocol-http (~> 0.58)
|
||||||
protocol-http2 (0.24.0)
|
protocol-http2 (0.24.0)
|
||||||
protocol-hpack (~> 1.4)
|
protocol-hpack (~> 1.4)
|
||||||
@@ -97,4 +97,4 @@ DEPENDENCIES
|
|||||||
win32-security
|
win32-security
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
4.0.3
|
2.6.8
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
# Documentation
|
|
||||||
|
|
||||||
## File Formats
|
|
||||||
|
|
||||||
Documentation and examples of file formats.
|
|
||||||
|
|
||||||
### [Legacy Manifest](file_formats/LEGACY_MANIFEST.md)
|
|
||||||
|
|
||||||
Blue Hell Productions `manifest.xml`. Still in use by W3D Hub.
|
|
||||||
|
|
||||||
### [Megafest](file_formats/MEGAFEST.md)
|
|
||||||
|
|
||||||
Prototype new thing `megafest.json`
|
|
||||||
|
|
||||||
### [MIX](file_formats/MIX.md)
|
|
||||||
|
|
||||||
Westwood archive format `<name>.mix`, `<name>.dat` and `<name>.pkg`
|
|
||||||
|
|
||||||
### [W3D Hub Patch](file_formats/W3D_HUB_PATCH.md)
|
|
||||||
|
|
||||||
Describes how to update a MIX archive from a package patch
|
|
||||||
|
|
||||||
## Protocols
|
|
||||||
|
|
||||||
### [LAN Discovery](protocols/LAN_DISCOVERY.md)
|
|
||||||
|
|
||||||
How launchers and other applications discovery each other.
|
|
||||||
|
|
||||||
### [Launcher Remote](protocols/LAUNCHER_REMOTE.md)
|
|
||||||
|
|
||||||
Remotely control and get progress reports from the launcher
|
|
||||||
|
|
||||||
### [LAN Package Share](protocols/LAN_PACKAGE_SHARE.md)
|
|
||||||
|
|
||||||
Enable launchers on the same network to discover each other and share packages over LAN.
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Legacy Manifest
|
|
||||||
|
|
||||||
In use since Blue Hell Productions
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> References to packages exclude `.zip`
|
|
||||||
|
|
||||||
Example `Full` manifest:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
|
|
||||||
<BHP_Game_Manifest game="apb" version="0.9935.1.0" type="Full">
|
|
||||||
<Dependency name="msvc-2015.zip"/>
|
|
||||||
<Dependency name="directx-43.zip"/>
|
|
||||||
<File name="game.exe" checksum="C7AE972A2B9CF6EFCADE7919323F048F5CFAF2D40BD5A67D5A91F8D8A3759CA4" package="binaries"/>
|
|
||||||
<File name="MemoryManager.dll" checksum="B60A2B3F6A07D729EE5C2902CB9A6BEDAF55EEDB13F8514BAC0AD6F5D51DD0C2" package="binaries"/>
|
|
||||||
<File name="scripts.dll" checksum="76CAAEBF21E4D6A34C3ABDD474D59580DD1EAB097C9585A1FFC0E28FF0FCAF56" package="binaries"/>
|
|
||||||
<File name="Scripts2.dll" checksum="A0439B3BFE6CB497F402291548A312A9F43C0D38BDAF26C83E21DBD3F4D55702" package="binaries"/>
|
|
||||||
<File name="shared.dll" checksum="17A8797EC5F4C34E241681F4AF76EEA93856D6A1854EC9838B263AEC89CDF036" package="binaries"/>
|
|
||||||
<File name="ttle.dll" checksum="C005EB50D77675CE712B30A4402AB0D152D3993EB509E35AEDCB834CED54FADF" package="binaries"/>
|
|
||||||
<File name="ttversion.txt" checksum="17A9E627A03E20AC33849330468BF8A80B48449C9D473DF45F1052A160083167" package="misc"/>
|
|
||||||
<File name="Data/always.dat" checksum="D4D09149C7B5368AB3947AEC7B6E7781208F592FCA4390261F04A8E9C1887EB3" package="always"/>
|
|
||||||
<File name="Data/RA_Volcano.mix" checksum="4F3CB80BCF200659B3B4B167CCB64ECB36A3BA917681F4AB8027568EC187B88D" package="RA_Volcano"/>
|
|
||||||
<File name="Data/Movies/ea_ww.bik" checksum="065B8B0894CF9C1C09A940D26069C54A771EBB3092143BA37E97F8CA85B8CCAD" package="movies"/>
|
|
||||||
<File name="Data/Movies/R_Intro.BIK" checksum="0D9A2B2ABFD9680DF1C8D0F876DA4B60F015CA5C56EC6C0593528A3F88E2A58F" package="movies"/>
|
|
||||||
</BHP_Game_Manifest>
|
|
||||||
```
|
|
||||||
|
|
||||||
Example `Patch` manifest:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
|
|
||||||
<BHP_Game_Manifest game="apb" version="3.6.4.1" type="Patch" baseVersion="3.6.4.0">
|
|
||||||
<Dependency name="msvc-2022.zip"/>
|
|
||||||
<Dependency name="msvc-2015.zip" removedsince="3.6.4.1"/>
|
|
||||||
<File name="data/Always.dat" checksum="7B27D547E5A50401C9CA2851ACD4AC3F3CDA104F34650E1C32EB63A4120E3C90">
|
|
||||||
<Patch from="3.6.4.0" package="Always.patch.3.6.4.0"/>
|
|
||||||
</File>
|
|
||||||
<File name="data/Always_Emitters.dat" checksum="5C3EA8E5C4D7278F5EF1AF3CE1FC4F698C5A7D1FE7116DA0376A709E7CD3B2AE">
|
|
||||||
<Patch from="3.6.3.0" package="Always_Emitters.patch.3.6.3.0"/>
|
|
||||||
</File>
|
|
||||||
<File name="data/Always_Vehicles.dat" checksum="C6D4F6B3412C26065DE9B9F739BEC041B49FD613DB2686D8F231C2F692B263C5">
|
|
||||||
<Patch from="3.6.4.0" package="Always_Vehicles.patch.3.6.4.0"/>
|
|
||||||
</File>
|
|
||||||
<File name="data/RA_TestUnits.mix" removedsince="3.6.4.1"/>
|
|
||||||
<File name="data/RA_RidgeWar.mix" checksum="C3438C532C27F535CD958BEA9702160E952BC6CE58F9B43778F91DA23C6AE9FE" package="RA_RidgeWar"/>
|
|
||||||
<File name="data/RA_SwampOfIllusions.mix" checksum="A0787979664E5977A1949587382A3A84B9F1526F81934EBB7A3AC9334F9D4059">
|
|
||||||
<Patch from="3.6.4.0" package="RA_SwampOfIllusions.patch.3.6.4.0"/>
|
|
||||||
</File>
|
|
||||||
</BHP_Game_Manifest>
|
|
||||||
```
|
|
||||||
|
|
||||||
## SPECIFICATION
|
|
||||||
### BHP_GAME_MANIFEST (Root Element)
|
|
||||||
* **GAME** - Unique Application ID
|
|
||||||
* **VERSION** - Application Unique Version for Build
|
|
||||||
* **TYPE** - Type of Build
|
|
||||||
* **Full** - Full Build
|
|
||||||
* **Patch** - Patch Build
|
|
||||||
* **BASEVERSION** - *OPTIONAL* Version this build was based upon
|
|
||||||
|
|
||||||
### DEPENDENCY
|
|
||||||
* **NAME** - Package name of dependency sans file extension
|
|
||||||
* **REMOVEDSINCE** - _OPTIONAL_ No data. Not used anymore?
|
|
||||||
|
|
||||||
### FILE
|
|
||||||
* **NAME** - Name of file
|
|
||||||
* **CHECKSUM** - _OPTIONAL_ SHA256 hash of file
|
|
||||||
* **PACKAGE** - _OPTIONAL_ Package that contains this file. Not present on removed files or files that have been patched.
|
|
||||||
* **REMOVEDSINCE** - _OPTIONAL_ Version this package was removed in. If present, only _NAME_ and _REMOVEDSINCE_ will be present.
|
|
||||||
* **PATCH**
|
|
||||||
* **FROM** - Version that the file last changed
|
|
||||||
* **PACKAGE** - Name of package that contains this file.
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
> [!NOTE]
|
|
||||||
> DRAFT
|
|
||||||
|
|
||||||
# Megafest
|
|
||||||
|
|
||||||
The fat manifest for W3D Hub Packager and Launcher(s), represented as a JSON hash.
|
|
||||||
|
|
||||||
This **MEGAFEST** is intended to simplify patch generation and distribution by ensuring that
|
|
||||||
all the data the Packager and Launcher(s) need can be found in a **single** manifest file.
|
|
||||||
|
|
||||||
Speed up application packaging by only needing to scan and checksum the new builds files instead of needing
|
|
||||||
to maintain a pristine copy of the previous version to be diffed against.
|
|
||||||
|
|
||||||
Reduce the number of requests that the launcher(s) need to make to sort out how to download and
|
|
||||||
patch the application.
|
|
||||||
|
|
||||||
## Glossary
|
|
||||||
|
|
||||||
### File
|
|
||||||
|
|
||||||
An individual file on disk.
|
|
||||||
|
|
||||||
An individual file as part of a MIX archive.
|
|
||||||
|
|
||||||
### Package
|
|
||||||
|
|
||||||
A collection of one or more files in a compressed archive for efficiently distributing the application.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Packages _MAY_ be compressed using ZSTANDARD (.zst) in the future. Do not assume all packages are zip files.
|
|
||||||
>
|
|
||||||
> Package names in the megafest include the file extension for this reason.
|
|
||||||
|
|
||||||
## Example `megafest.json` File
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"spec": 0,
|
|
||||||
"application": {
|
|
||||||
"id": "apb",
|
|
||||||
"version": "3.7.0.1",
|
|
||||||
"user_level": "public",
|
|
||||||
"previous_versions": [
|
|
||||||
"3.7.0.0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": [
|
|
||||||
"msvc-2022"
|
|
||||||
],
|
|
||||||
"packages": [
|
|
||||||
{
|
|
||||||
"name": "binaries.zip",
|
|
||||||
"version": "3.7.0.0",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 4096,
|
|
||||||
"chunk_size": 4194304,
|
|
||||||
"chunk_checksums": [
|
|
||||||
"SHA256-HASH-A",
|
|
||||||
"SHA256-HASH-B"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "game2.exe",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 8691743
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data/Always.dat.zip",
|
|
||||||
"version": "3.7.0.0",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 1355917483,
|
|
||||||
"chunk_size": 4194304,
|
|
||||||
"chunk_checksums": [
|
|
||||||
"SHA256-HASH-A",
|
|
||||||
"SHA256-HASH-B"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "mp_wep_gdi.w3d",
|
|
||||||
"checksum": "CRC32-HASH",
|
|
||||||
"offset": 0,
|
|
||||||
"size": 8691743
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data/Always.patch.3.7.0.0.zst",
|
|
||||||
"version": "3.7.0.1",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 1355919483,
|
|
||||||
"chunk_size": 4194304,
|
|
||||||
"chunk_checksums": [
|
|
||||||
"SHA256-HASH-A",
|
|
||||||
"SHA256-HASH-B"
|
|
||||||
],
|
|
||||||
"from_version": "3.7.0.0",
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "data/Always.patch",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 8691743
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "binaries.zip",
|
|
||||||
"version": "3.7.0.1",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 4096,
|
|
||||||
"chunk_size": 4194304,
|
|
||||||
"chunk_checksums": [
|
|
||||||
"SHA256-HASH-A",
|
|
||||||
"SHA256-HASH-B"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "game.exe",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 8691743
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"changes": [
|
|
||||||
{
|
|
||||||
"name": "game.exe",
|
|
||||||
"type": "added",
|
|
||||||
"package": "binaries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "game2.exe",
|
|
||||||
"type": "removed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data/Always.dat",
|
|
||||||
"type": "updated",
|
|
||||||
"package": "Always.dat"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index": [
|
|
||||||
{
|
|
||||||
"name": "game.exe",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 8691743
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data/Always.dat",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 1355917483
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## SPECIFICATION
|
|
||||||
|
|
||||||
### Application
|
|
||||||
|
|
||||||
List of application details:
|
|
||||||
|
|
||||||
* **ID** - Unique Application ID
|
|
||||||
* **VERSION** - Application Unique Version
|
|
||||||
* **USER_LEVEL** - User access level for this version
|
|
||||||
* **PREVIOUS_VERSIONS** - Complete list of previous versions. Full builds only include _the_ previous version
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "apb",
|
|
||||||
"version": "3.7.0.1",
|
|
||||||
"user_level": "public",
|
|
||||||
"previous_versions": [
|
|
||||||
"3.7.0.0"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
List of application's dependencies
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
"msvc-2022"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Packages
|
|
||||||
|
|
||||||
Complete list of application's version tree of packages.
|
|
||||||
|
|
||||||
That is, every package from the last full build to present for this version series.
|
|
||||||
|
|
||||||
* **NAME** - Name of package
|
|
||||||
* **VERSION** - Application version this package belongs to
|
|
||||||
* **CHECKSUM** - SHA256 checksum of package
|
|
||||||
* **SIZE** - File size of package in bytes
|
|
||||||
* CHUNK **SIZE** - Size _CHUNK_CHECKSUMS_ represent in bytes
|
|
||||||
* **CHUNK_CHECKSUMS** - Array of file chunk checksums
|
|
||||||
* **FROM_VERSION** - *OPTIONAL* Version this file was last changed. Only present for MIX patches.
|
|
||||||
* **FILES** - Array of files
|
|
||||||
* **NAME**
|
|
||||||
* **CHECKSUM** - SHA256 checksum of file.
|
|
||||||
* **SIZE** - File size in bytes
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "binaries",
|
|
||||||
"version": "3.7.0.0",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 4096,
|
|
||||||
"chunk_size": 4194304,
|
|
||||||
"chunk_checksums": [
|
|
||||||
"SHA256-HASH-A",
|
|
||||||
"SHA256-HASH-B"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "game.exe",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 8691743
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data/Always.patch.3.7.0.0",
|
|
||||||
"version": "3.7.0.1",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 2436543,
|
|
||||||
"chunk_size": 4194304,
|
|
||||||
"chunk_checksums": [
|
|
||||||
"SHA256-HASH-A",
|
|
||||||
"SHA256-HASH-B"
|
|
||||||
],
|
|
||||||
"from_version": "3.7.0.0",
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "data/Always.patch",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 3636749
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
List of file changes in this build.
|
|
||||||
|
|
||||||
* **NAME** - Name of file
|
|
||||||
* **TYPE** - Type of change
|
|
||||||
* **ADDED** - New file has been added
|
|
||||||
* **UPDATED** - File has been changed
|
|
||||||
* **REMOVED** - File has been deleted
|
|
||||||
* **PACKAGE** - _OPTIONAL_ Name of package, for this build version, the file can be found in
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "game.exe",
|
|
||||||
"type": "updated",
|
|
||||||
"package": "binaries.zip"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "game2.exe",
|
|
||||||
"type": "removed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data/Always.dat",
|
|
||||||
"type": "added",
|
|
||||||
"package": "Always.dat.zip"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Index
|
|
||||||
|
|
||||||
Complete list of files for this build with: filename, checksum, file size, and list of files for MIX archives.
|
|
||||||
|
|
||||||
* **NAME** - File name
|
|
||||||
* **CHECKSUM** - SHA256 hash of file
|
|
||||||
* **SIZE** - File size
|
|
||||||
* **FILES** - _OPTIONAL_ List of files inside of MIX archives
|
|
||||||
* **NAME** - File name
|
|
||||||
* **CHECKSUM** - CRC32 hash of file data
|
|
||||||
* **OFFSET** - Offset of file in MIX archive
|
|
||||||
* **SIZE** - Size of file data
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "game.exe",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 8691743
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data/Always.dat",
|
|
||||||
"checksum": "SHA256-HASH",
|
|
||||||
"size": 570961432,
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "mp_wep_gdi.w3d",
|
|
||||||
"checksum": "CRC32_HASH",
|
|
||||||
"offset": 0,
|
|
||||||
"size": 3524
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# Westwood Renegade MIX Archive
|
|
||||||
|
|
||||||
with W3D Hub / Tiberian Technologies alterations
|
|
||||||
|
|
||||||
The .MIX archive file format is used by Renegade as _yes_.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> The MIX format has no notion about directories.
|
|
||||||
>
|
|
||||||
> Each file name **MUST** be unique.
|
|
||||||
>
|
|
||||||
> e.g. `data/map.dds` and `data/RA_Under/MAP.dds` have the same base file name
|
|
||||||
> and therefore **MUST** raise an error when writing.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> `io_pos` is the current position on the seek head of the open file
|
|
||||||
|
|
||||||
## Reading
|
|
||||||
|
|
||||||
* Read [Header](#header)
|
|
||||||
* validate MIME type (`MIX1` or `MIX2`)
|
|
||||||
* Jump to io_pos `file_data_offset`
|
|
||||||
* Read `file_count`
|
|
||||||
* Read array of [`file_data`](#file-data-array)
|
|
||||||
* Jump to io_pos `file_names_offset`
|
|
||||||
* Read `file_count`
|
|
||||||
* Read array of [`file_names`](#file-names-array)
|
|
||||||
* Read file blobs
|
|
||||||
* For each file's [`file_data`](#file-data-array)
|
|
||||||
* Jump io_pos to `file_content_offset`
|
|
||||||
* Read `file_content_length`
|
|
||||||
* Done.
|
|
||||||
|
|
||||||
## Writing
|
|
||||||
|
|
||||||
* Write MIME type (`MIX1` or `MIX2`)
|
|
||||||
* Jump to io_pos `16`
|
|
||||||
* Write file blobs
|
|
||||||
* **IMPORTANT:** Add `-io_pos & 7` padding to io_pos **AFTER** each file blob is written
|
|
||||||
* Save io_pos as `file_data_offset`
|
|
||||||
* Write `file_count`
|
|
||||||
* Write array of [`file_data`](#file-data-array)
|
|
||||||
* Save io_pos as `file_names_offset`
|
|
||||||
* Write `file_count`
|
|
||||||
* Write array of [`file_names`](#file-names-array)
|
|
||||||
* **IMPORTANT:** Ensure file names are null terminated and DO NOT exceed 254 (+ null byte) characters in length.
|
|
||||||
* Jump to io_pos `4`
|
|
||||||
* Write [`file_data_offset`](#header)
|
|
||||||
* Write [`file_names_offset`](#header)
|
|
||||||
* Write [`reserved`](#header)
|
|
||||||
* Done.
|
|
||||||
|
|
||||||
## Pseudo Example:
|
|
||||||
|
|
||||||
```yml
|
|
||||||
header:
|
|
||||||
mime: MIX1 or MIX2
|
|
||||||
file_data_offset: 0x1024EAEA
|
|
||||||
file_names_offset: 0x8192EAEA
|
|
||||||
reserved: 0
|
|
||||||
|
|
||||||
files:
|
|
||||||
array:
|
|
||||||
- file_blob
|
|
||||||
|
|
||||||
file_data:
|
|
||||||
file_count: 0x000021
|
|
||||||
array:
|
|
||||||
file_name_crc32: 0xEFEFEFEF
|
|
||||||
file_content_offset: 0x3217DEAD
|
|
||||||
file_content_length: 0x0018BEEF
|
|
||||||
|
|
||||||
file_names:
|
|
||||||
file_count: 0x000021
|
|
||||||
array:
|
|
||||||
file_name_length: 0xff
|
|
||||||
file_name: "mp_wep_gdi.w3d"
|
|
||||||
```
|
|
||||||
|
|
||||||
## MIME Types
|
|
||||||
|
|
||||||
### MIX1 `0x3158494D`
|
|
||||||
|
|
||||||
Westwood Farm Fresh Organic.
|
|
||||||
|
|
||||||
### MIX2 `0x3258494D`
|
|
||||||
|
|
||||||
Same as `MIX1`. `MIX2` hints to the engine's file reader to decrypt files before use.
|
|
||||||
|
|
||||||
## Data Structures
|
|
||||||
|
|
||||||
### Header
|
|
||||||
|
|
||||||
| Name | Offset | Type | Description |
|
|
||||||
|-------------------|--------|---------|---------------------------------------------------------------------|
|
|
||||||
| MIME | 0 | int32_t | 4 bytes representing `MIX1` (`0x3158494D)` or `MIX2` (`0x3258494D`) |
|
|
||||||
| File Data Offset | 4 | int32_t | Offset in MIX archive that [`file_data`](#file-data) data starts |
|
|
||||||
| File Names Offset | 8 | int32_t | Offset in MIX archive that [`file_name`](#file-names) data starts |
|
|
||||||
| RESERVED | 12 | int32_t | Unused reserved int. Write as `0` |
|
|
||||||
|
|
||||||
### File
|
|
||||||
|
|
||||||
| Name | Offset | Type | Description |
|
|
||||||
|-----------|---------|-----------------------------|---------------|
|
|
||||||
| File Blob | complex | char[`file_content_length`] | File contents |
|
|
||||||
|
|
||||||
### File Data
|
|
||||||
|
|
||||||
| Name | Offset | Type | Description |
|
|
||||||
|------------|--------------------|---------|--------------------------------|
|
|
||||||
| File Count | `file_data_offset` | int32_t | Number of files in MIX archive |
|
|
||||||
|
|
||||||
### File Data Array
|
|
||||||
|
|
||||||
| Name | Offset | Type | Description |
|
|
||||||
|---------------------|--------------------------------------------|----------|--------------------------------------------------------|
|
|
||||||
| File Name CRC32 | `file_data_offset` + 4 * (index * sizeof) | uint32_t | CRC32 of **UPPERCASE** file name in network byte order |
|
|
||||||
| File Content Offset | `file_data_offset` + 8 * (index * sizeof) | uint32_t | File content offset |
|
|
||||||
| File Content Length | `file_data_offset` + 12 * (index * sizeof) | uint32_t | File content length |
|
|
||||||
|
|
||||||
### File Names
|
|
||||||
|
|
||||||
| Name | Offset | Type | Description |
|
|
||||||
|------------|---------------------|---------|--------------------------------|
|
|
||||||
| File Count | `file_names_offset` | int32_t | Number of files in MIX archive |
|
|
||||||
|
|
||||||
### File Names Array
|
|
||||||
|
|
||||||
| Name | Offset | Type | Description |
|
|
||||||
|------------------|---------|--------------------------|-------------------------------------------------|
|
|
||||||
| File Name Length | complex | uint8_t | Length of string in bytes, including null byte. |
|
|
||||||
| File Name | complex | char[`file_name_length`] | null terminated string |
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# W3D Hub Patch
|
|
||||||
A W3D Hub Patch
|
|
||||||
|
|
||||||
A `.w3dhub.patch` is included in MIX Patch files.
|
|
||||||
It contains instructions for updating the target MIX.
|
|
||||||
|
|
||||||
JSON file with a static CRC32 of `E6FE46B8`
|
|
||||||
|
|
||||||
## Example:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"removedFiles": [
|
|
||||||
"sov_v_mad.gif"
|
|
||||||
],
|
|
||||||
"updatedFiles": [
|
|
||||||
"sov_v_mad.w3d"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
> [!NOTE]
|
|
||||||
> DRAFT
|
|
||||||
|
|
||||||
# LAN Discovery
|
|
||||||
|
|
||||||
Doing things
|
|
||||||
|
|
||||||
Broadcast port: 4898
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
|-------------------|----------|--------------------------------------------------------------------------|
|
|
||||||
| Version | int32_t | Version of protocol this application supports |
|
|
||||||
| Owner | string | Nickname of application's active user |
|
|
||||||
| Hostname | string | Name of device |
|
|
||||||
| UUID | string | Unique identifier for this application |
|
|
||||||
| Application | string | Name of application |
|
|
||||||
| Features | array | Array of strings naming features the application supports |
|
|
||||||
| Service Port | uint16_t | Dynamically assigned TCP port that interested parties should connect too |
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Max packet size for UDP broadcast may need to be limited to 512 bytes
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": 0,
|
|
||||||
"owner": "cyberarm",
|
|
||||||
"hostname": "PC-1692",
|
|
||||||
"uuid": "019bcf1e-a22e-7fe0-a3db-3a9d37bfc6fa",
|
|
||||||
"application": "W3D Hub Linux Launcher",
|
|
||||||
"features": [
|
|
||||||
"launcher_remote:3",
|
|
||||||
"package_share:1"
|
|
||||||
],
|
|
||||||
"service_port": 56802
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
> [!NOTE]
|
|
||||||
> DRAFT
|
|
||||||
|
|
||||||
# LAN Package Share
|
|
||||||
|
|
||||||
Authentication is not required, only user opt-in to enable package sharing from their application.
|
|
||||||
|
|
||||||
* List available packages
|
|
||||||
* upload or download verified packages between other applications on LAN network
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
> [!NOTE]
|
|
||||||
> DRAFT
|
|
||||||
|
|
||||||
# Launcher Remote
|
|
||||||
|
|
||||||
* Get list of available, installed, and in progress applications
|
|
||||||
* Trigger download or update of application
|
|
||||||
* Delete application
|
|
||||||
* Join server
|
|
||||||
298
lib/api.rb
298
lib/api.rb
@@ -16,7 +16,31 @@ class W3DHub
|
|||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
def self.on_thread(method, *args, &callback)
|
def self.on_thread(method, *args, &callback)
|
||||||
Api.send(method, *args, &callback)
|
BackgroundWorker.foreground_job(-> { Api.send(method, *args) }, callback)
|
||||||
|
end
|
||||||
|
|
||||||
|
class Response
|
||||||
|
def initialize(error: nil, status: -1, body: "")
|
||||||
|
@status = status
|
||||||
|
@body = body
|
||||||
|
@error = error
|
||||||
|
end
|
||||||
|
|
||||||
|
def success?
|
||||||
|
@status == 200
|
||||||
|
end
|
||||||
|
|
||||||
|
def status
|
||||||
|
@status
|
||||||
|
end
|
||||||
|
|
||||||
|
def body
|
||||||
|
@body
|
||||||
|
end
|
||||||
|
|
||||||
|
def error
|
||||||
|
@error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
#! === W3D Hub API === !#
|
#! === W3D Hub API === !#
|
||||||
@@ -25,9 +49,7 @@ class W3DHub
|
|||||||
|
|
||||||
HTTP_CLIENTS = {}
|
HTTP_CLIENTS = {}
|
||||||
|
|
||||||
def self.async_http(method, path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub, &callback)
|
def self.async_http(method, path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
|
||||||
raise "NO CALLBACK DEFINED!" unless callback
|
|
||||||
|
|
||||||
case backend
|
case backend
|
||||||
when :w3dhub
|
when :w3dhub
|
||||||
endpoint = W3DHUB_API_ENDPOINT
|
endpoint = W3DHUB_API_ENDPOINT
|
||||||
@@ -38,15 +60,15 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Handle arbitrary urls that may come through
|
# Handle arbitrary urls that may come through
|
||||||
|
url = nil
|
||||||
if path.start_with?("http")
|
if path.start_with?("http")
|
||||||
uri = URI(path)
|
uri = URI(path)
|
||||||
|
|
||||||
endpoint = uri.origin
|
endpoint = uri.origin
|
||||||
path = uri.request_uri
|
path = uri.request_uri
|
||||||
|
else
|
||||||
|
url = "#{endpoint}#{path}"
|
||||||
end
|
end
|
||||||
|
|
||||||
url = "#{endpoint}#{path}"
|
|
||||||
|
|
||||||
logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." }
|
logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." }
|
||||||
|
|
||||||
# Inject Authorization header if account data is populated
|
# Inject Authorization header if account data is populated
|
||||||
@@ -56,7 +78,24 @@ class W3DHub
|
|||||||
headers << ["authorization", "Bearer #{Store.account.access_token}"]
|
headers << ["authorization", "Bearer #{Store.account.access_token}"]
|
||||||
end
|
end
|
||||||
|
|
||||||
Store.network_manager.request(method, url, headers, body, nil, &callback)
|
Sync do
|
||||||
|
begin
|
||||||
|
response = provision_http_client(endpoint).send(method, path, headers, body)
|
||||||
|
|
||||||
|
Response.new(status: response.status, body: response.read)
|
||||||
|
rescue Async::TimeoutError => e
|
||||||
|
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
|
||||||
|
|
||||||
|
Response.new(error: e)
|
||||||
|
rescue StandardError => e
|
||||||
|
logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" }
|
||||||
|
logger.error(LOG_TAG) { e }
|
||||||
|
|
||||||
|
Response.new(error: e)
|
||||||
|
ensure
|
||||||
|
response&.close
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.provision_http_client(hostname)
|
def self.provision_http_client(hostname)
|
||||||
@@ -74,17 +113,17 @@ class W3DHub
|
|||||||
HTTP_CLIENTS[Thread.current][hostname.downcase] = Async::HTTP::Client.new(endpoint)
|
HTTP_CLIENTS[Thread.current][hostname.downcase] = Async::HTTP::Client.new(endpoint)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub, &callback)
|
def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
|
||||||
async_http(:post, path, headers, body, backend, &callback)
|
async_http(:post, path, headers, body, backend)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.get(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub, &callback)
|
def self.get(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
|
||||||
async_http(:get, path, headers, body, backend, &callback)
|
async_http(:get, path, headers, body, backend)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Api.get but handles any URL instead of known hosts
|
# Api.get but handles any URL instead of known hosts
|
||||||
def self.fetch(path, headers = DEFAULT_HEADERS, body = nil, backend = nil, &callback)
|
def self.fetch(path, headers = DEFAULT_HEADERS, body = nil, backend = nil)
|
||||||
async_http(:get, path, headers, body, backend, &callback)
|
async_http(:get, path, headers, body, backend)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Method: POST
|
# Method: POST
|
||||||
@@ -102,111 +141,90 @@ class W3DHub
|
|||||||
#
|
#
|
||||||
# On a failed login the service responds with:
|
# On a failed login the service responds with:
|
||||||
# {"error":"login-failed"}
|
# {"error":"login-failed"}
|
||||||
def self.refresh_user_login(refresh_token, backend = :w3dhub, &callback)
|
def self.refresh_user_login(refresh_token, backend = :w3dhub)
|
||||||
handler = lambda do |result|
|
body = URI.encode_www_form("data": JSON.dump({refreshToken: refresh_token}))
|
||||||
if result.okay?
|
response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend)
|
||||||
user_data = JSON.parse(result.data, symbolize_names: true)
|
|
||||||
|
|
||||||
if user_data[:error]
|
if response.status == 200
|
||||||
callback.call(false)
|
user_data = JSON.parse(response.body, symbolize_names: true)
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
user_details_data = user_details(user_data[:userid]) || {}
|
return false if user_data[:error]
|
||||||
|
|
||||||
callback.call(Account.new(user_data, user_details_data))
|
user_details_data = user_details(user_data[:userid]) || {}
|
||||||
else
|
|
||||||
logger.error(LOG_TAG) { "Failed to fetch refresh user login:" }
|
|
||||||
logger.error(LOG_TAG) { result.error }
|
|
||||||
|
|
||||||
callback.call(false)
|
Account.new(user_data, user_details_data)
|
||||||
end
|
else
|
||||||
|
logger.error(LOG_TAG) { "Failed to fetch refresh user login:" }
|
||||||
|
logger.error(LOG_TAG) { response }
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
body = URI.encode_www_form("data": JSON.dump({ refreshToken: refresh_token }))
|
|
||||||
post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend, &handler)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# See #user_refresh_token
|
# See #user_refresh_token
|
||||||
def self.user_login(username, password, backend = :w3dhub, &callback)
|
def self.user_login(username, password, backend = :w3dhub)
|
||||||
handler = lambda do |result|
|
body = URI.encode_www_form("data": JSON.dump({username: username, password: password}))
|
||||||
if result.okay?
|
response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend)
|
||||||
user_data = JSON.parse(result.data, symbolize_names: true)
|
|
||||||
|
|
||||||
if user_data[:error]
|
if response.status == 200
|
||||||
callback.call(false)
|
user_data = JSON.parse(response.body, symbolize_names: true)
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
user_details_data = user_details(user_data[:userid]) || {}
|
return false if user_data[:error]
|
||||||
|
|
||||||
callback.call(Account.new(user_data, user_details_data))
|
user_details_data = user_details(user_data[:userid]) || {}
|
||||||
else
|
|
||||||
logger.error(LOG_TAG) { "Failed to fetch user login:" }
|
|
||||||
logger.error(LOG_TAG) { result.error }
|
|
||||||
|
|
||||||
callback.call(false)
|
Account.new(user_data, user_details_data)
|
||||||
end
|
else
|
||||||
|
logger.error(LOG_TAG) { "Failed to fetch user login:" }
|
||||||
|
logger.error(LOG_TAG) { response }
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
body = URI.encode_www_form("data": JSON.dump({ username: username, password: password }))
|
|
||||||
post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend, &handler)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# /apis/w3dhub/1/get-user-details
|
# /apis/w3dhub/1/get-user-details
|
||||||
#
|
#
|
||||||
# Response: avatar-uri (Image download uri), id, username
|
# Response: avatar-uri (Image download uri), id, username
|
||||||
def self.user_details(id, backend = :w3dhub, &callback)
|
def self.user_details(id, backend = :w3dhub)
|
||||||
handler = lambda do |result|
|
|
||||||
if result.okay?
|
|
||||||
callback.call(JSON.parse(result.data, symbolize_names: true))
|
|
||||||
else
|
|
||||||
logger.error(LOG_TAG) { "Failed to fetch user details:" }
|
|
||||||
logger.error(LOG_TAG) { result.error }
|
|
||||||
|
|
||||||
callback.call(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
body = URI.encode_www_form("data": JSON.dump({ id: id }))
|
body = URI.encode_www_form("data": JSON.dump({ id: id }))
|
||||||
post("/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body, backend, &handler)
|
user_details = post("/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body, backend)
|
||||||
|
|
||||||
|
if user_details.status == 200
|
||||||
|
JSON.parse(user_details.body, symbolize_names: true)
|
||||||
|
else
|
||||||
|
logger.error(LOG_TAG) { "Failed to fetch user details:" }
|
||||||
|
logger.error(LOG_TAG) { user_details }
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# /apis/w3dhub/1/get-service-status
|
# /apis/w3dhub/1/get-service-status
|
||||||
# Service response:
|
# Service response:
|
||||||
# {"services":{"authentication":true,"packageDownload":true}}
|
# {"services":{"authentication":true,"packageDownload":true}}
|
||||||
def self.service_status(backend = :w3dhub, &callback)
|
def self.service_status(backend = :w3dhub)
|
||||||
handler = lambda do |result|
|
response = post("/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS, nil, backend)
|
||||||
if result.okay?
|
|
||||||
callback.call(ServiceStatus.new(result.data))
|
|
||||||
else
|
|
||||||
logger.error(LOG_TAG) { "Failed to fetch service status:" }
|
|
||||||
logger.error(LOG_TAG) { result.error }
|
|
||||||
|
|
||||||
callback.call(false)
|
if response.status == 200
|
||||||
end
|
ServiceStatus.new(response.body)
|
||||||
|
else
|
||||||
|
logger.error(LOG_TAG) { "Failed to fetch service status:" }
|
||||||
|
logger.error(LOG_TAG) { response }
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
post("/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS, nil, backend, &handler)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# /apis/launcher/1/get-applications
|
# /apis/launcher/1/get-applications
|
||||||
# Client sends an Authorization header bearer token which is received from logging in (Optional)
|
# Client sends an Authorization header bearer token which is received from logging in (Optional)
|
||||||
# Launcher sends an empty data request: data={}
|
# Launcher sends an empty data request: data={}
|
||||||
# Response is a list of applications/games
|
# Response is a list of applications/games
|
||||||
def self.applications(backend = :w3dhub, &callback)
|
def self.applications(backend = :w3dhub)
|
||||||
handler = lambda do |result|
|
response = post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend)
|
||||||
if result.okay?
|
|
||||||
callback.call(Applications.new(result.data, backend))
|
|
||||||
else
|
|
||||||
logger.error(LOG_TAG) { "Failed to fetch applications list:" }
|
|
||||||
logger.error(LOG_TAG) { result.error }
|
|
||||||
|
|
||||||
callback.call(false)
|
if response.status == 200
|
||||||
end
|
Applications.new(response.body, backend)
|
||||||
|
else
|
||||||
|
logger.error(LOG_TAG) { "Failed to fetch applications list:" }
|
||||||
|
logger.error(LOG_TAG) { response }
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend, &handler)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Populate applications list from primary and alternate backends
|
# Populate applications list from primary and alternate backends
|
||||||
@@ -282,70 +300,61 @@ class W3DHub
|
|||||||
# Client sends an Authorization header bearer token which is received from logging in (Optional)
|
# Client sends an Authorization header bearer token which is received from logging in (Optional)
|
||||||
# Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates)
|
# Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates)
|
||||||
# Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one
|
# Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one
|
||||||
def self.news(category, backend = :w3dhub, &callback)
|
def self.news(category, backend = :w3dhub)
|
||||||
handler = lambda do |result|
|
body = URI.encode_www_form("data": JSON.dump({category: category}))
|
||||||
if result.okay?
|
response = post("/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body, backend)
|
||||||
callback.call(News.new(result.data))
|
|
||||||
else
|
|
||||||
logger.error(LOG_TAG) { "Failed to fetch news for:" }
|
|
||||||
logger.error(LOG_TAG) { category }
|
|
||||||
logger.error(LOG_TAG) { result.error }
|
|
||||||
|
|
||||||
callback.call(false)
|
if response.status == 200
|
||||||
end
|
News.new(response.body)
|
||||||
|
else
|
||||||
|
logger.error(LOG_TAG) { "Failed to fetch news for:" }
|
||||||
|
logger.error(LOG_TAG) { category }
|
||||||
|
logger.error(LOG_TAG) { response }
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
body = URI.encode_www_form("data": JSON.dump({ category: category }))
|
|
||||||
post("/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body, backend, &handler)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Downloading games
|
# Downloading games
|
||||||
|
|
||||||
# /apis/launcher/1/get-package-details
|
# /apis/launcher/1/get-package-details
|
||||||
# client requests package details: data={"packages":[{"category":"games","name":"apb.ico","subcategory":"apb","version":""}]}
|
# client requests package details: data={"packages":[{"category":"games","name":"apb.ico","subcategory":"apb","version":""}]}
|
||||||
def self.package_details(packages, backend = :w3dhub, &callback)
|
def self.package_details(packages, backend = :w3dhub)
|
||||||
handler = lambda do |result|
|
|
||||||
if result.okay?
|
|
||||||
hash = JSON.parse(result.data, symbolize_names: true)
|
|
||||||
|
|
||||||
callback.call(hash[:packages].map { |pkg| Package.new(pkg) })
|
|
||||||
else
|
|
||||||
logger.error(LOG_TAG) { "Failed to fetch package details for:" }
|
|
||||||
logger.error(LOG_TAG) { packages }
|
|
||||||
logger.error(LOG_TAG) { result.error }
|
|
||||||
|
|
||||||
callback.call(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
body = URI.encode_www_form("data": JSON.dump({ packages: packages }))
|
body = URI.encode_www_form("data": JSON.dump({ packages: packages }))
|
||||||
post("/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body, backend, &handler)
|
response = post("/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body, backend)
|
||||||
|
|
||||||
|
if response.status == 200
|
||||||
|
hash = JSON.parse(response.body, symbolize_names: true)
|
||||||
|
|
||||||
|
hash[:packages].map { |pkg| Package.new(pkg) }
|
||||||
|
else
|
||||||
|
logger.error(LOG_TAG) { "Failed to fetch package details for:" }
|
||||||
|
logger.error(LOG_TAG) { packages }
|
||||||
|
logger.error(LOG_TAG) { response }
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# /apis/launcher/1/get-package
|
# /apis/launcher/1/get-package
|
||||||
# client requests package: data={"category":"games","name":"ECW_Asteroids.zip","subcategory":"ecw","version":"1.0.0.0"}
|
# client requests package: data={"category":"games","name":"ECW_Asteroids.zip","subcategory":"ecw","version":"1.0.0.0"}
|
||||||
#
|
#
|
||||||
# server responds with download bytes, probably supports chunked download and resume
|
# server responds with download bytes, probably supports chunked download and resume
|
||||||
# FIXME: REFACTOR Cache.fetch_package to use HttpClient
|
def self.package(package, &block)
|
||||||
def self.package(package, &callback)
|
Cache.fetch_package(package, block)
|
||||||
Cache.fetch_package(package, callback)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# /apis/w3dhub/1/get-events
|
# /apis/w3dhub/1/get-events
|
||||||
#
|
#
|
||||||
# clients requests events: data={"serverPath":"apb"}
|
# clients requests events: data={"serverPath":"apb"}
|
||||||
def self.events(app_id, backend = :w3dhub, &callback)
|
def self.events(app_id, backend = :w3dhub)
|
||||||
handler = lambda do |result|
|
|
||||||
if result.okay?
|
|
||||||
array = JSON.parse(response.body, symbolize_names: true)
|
|
||||||
callback.call(array.map { |e| Event.new(e) })
|
|
||||||
else
|
|
||||||
callback.call(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
body = URI.encode_www_form("data": JSON.dump({ serverPath: app_id }))
|
body = URI.encode_www_form("data": JSON.dump({ serverPath: app_id }))
|
||||||
post("/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body, backend, &handler)
|
response = post("/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body, backend)
|
||||||
|
|
||||||
|
if response.status == 200
|
||||||
|
array = JSON.parse(response.body, symbolize_names: true)
|
||||||
|
array.map { |e| Event.new(e) }
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
#! === Server List API === !#
|
#! === Server List API === !#
|
||||||
@@ -371,17 +380,15 @@ class W3DHub
|
|||||||
# id, name, score, kills, deaths
|
# id, name, score, kills, deaths
|
||||||
# ...players[]:
|
# ...players[]:
|
||||||
# nick, team (index of teams array), score, kills, deaths
|
# nick, team (index of teams array), score, kills, deaths
|
||||||
def self.server_list(level = 1, backend = :gsh, &callback)
|
def self.server_list(level = 1, backend = :gsh)
|
||||||
handler = lambda do |result|
|
response = get("/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend)
|
||||||
if result.okay?
|
|
||||||
data = JSON.parse(result.data, symbolize_names: true)
|
if response.status == 200
|
||||||
callback.call(data.map { |hash| ServerListServer.new(hash) })
|
data = JSON.parse(response.body, symbolize_names: true)
|
||||||
else
|
return data.map { |hash| ServerListServer.new(hash) }
|
||||||
callback.call(false)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
get("/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend, &handler)
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
# /listings/getStatus/v2/:id?statusLevel=#{0-2}
|
# /listings/getStatus/v2/:id?statusLevel=#{0-2}
|
||||||
@@ -395,24 +402,23 @@ class W3DHub
|
|||||||
# id, name, score, kills, deaths
|
# id, name, score, kills, deaths
|
||||||
# ...players[]:
|
# ...players[]:
|
||||||
# nick, team (index of teams array), score, kills, deaths
|
# nick, team (index of teams array), score, kills, deaths
|
||||||
def self.server_details(id, level, backend = :gsh, &callback)
|
def self.server_details(id, level, backend = :gsh)
|
||||||
return false unless id && level
|
return false unless id && level
|
||||||
|
|
||||||
handler = lambda do |result|
|
response = get("/listings/getStatus/v2/#{id}?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend)
|
||||||
if result.okay?
|
|
||||||
callback.call(JSON.parse(response.body, symbolize_names: true))
|
if response.status == 200
|
||||||
else
|
hash = JSON.parse(response.body, symbolize_names: true)
|
||||||
callback.call(false)
|
return hash
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
get("/listings/getStatus/v2/#{id}?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend, &handler)
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
# /listings/push/v2/negotiate?negotiateVersion=1
|
# /listings/push/v2/negotiate?negotiateVersion=1
|
||||||
##? /listings/push/v2/?id=#{websocket token?}
|
##? /listings/push/v2/?id=#{websocket token?}
|
||||||
## Websocket server list listener
|
## Websocket server list listener
|
||||||
def self.server_list_push(id, &callback)
|
def self.server_list_push(id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,12 +20,8 @@ class W3DHub
|
|||||||
|
|
||||||
@status = Status.new(@data[:status])
|
@status = Status.new(@data[:status])
|
||||||
|
|
||||||
# if we're on unix and using the PingManager then check every second since
|
@ping_interval = 30_000
|
||||||
# we're not _actually_ pinging the server.
|
|
||||||
@ping_interval = W3DHub.unix? ? 1_000 : 60_000
|
|
||||||
@last_pinged = Gosu.milliseconds + @ping_interval + 1_000
|
@last_pinged = Gosu.milliseconds + @ping_interval + 1_000
|
||||||
|
|
||||||
Store.ping_manager.add_address(@address)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(hash)
|
def update(hash)
|
||||||
@@ -53,16 +49,6 @@ class W3DHub
|
|||||||
if force_ping || Gosu.milliseconds - @last_pinged >= @ping_interval
|
if force_ping || Gosu.milliseconds - @last_pinged >= @ping_interval
|
||||||
@last_pinged = Gosu.milliseconds
|
@last_pinged = Gosu.milliseconds
|
||||||
|
|
||||||
if W3DHub.unix?
|
|
||||||
average_ping = Store.ping_manager.ping_for(@address)
|
|
||||||
|
|
||||||
@ping = average_ping.negative? ? NO_OR_BAD_PING : average_ping
|
|
||||||
|
|
||||||
States::Interface.instance&.update_server_ping(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
return unless W3DHub.windows?
|
|
||||||
|
|
||||||
W3DHub::BackgroundWorker.foreground_parallel_job(
|
W3DHub::BackgroundWorker.foreground_parallel_job(
|
||||||
lambda do
|
lambda do
|
||||||
W3DHub.command("ping #{@address} #{W3DHub.windows? ? '-n 3' : '-c 3'}") do |line|
|
W3DHub.command("ping #{@address} #{W3DHub.windows? ? '-n 3' : '-c 3'}") do |line|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class W3DHub
|
|||||||
@invocation_id = 0
|
@invocation_id = 0
|
||||||
|
|
||||||
logger.info(LOG_TAG) { "Starting emulated SignalR Server List Updater..." }
|
logger.info(LOG_TAG) { "Starting emulated SignalR Server List Updater..." }
|
||||||
# run
|
run
|
||||||
end
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
@@ -32,8 +32,7 @@ class W3DHub
|
|||||||
begin
|
begin
|
||||||
@auto_reconnect = true
|
@auto_reconnect = true
|
||||||
|
|
||||||
# FIXME
|
while W3DHub::BackgroundWorker.alive?
|
||||||
while true #W3DHub::BackgroundWorker.alive?
|
|
||||||
connect if @auto_reconnect
|
connect if @auto_reconnect
|
||||||
sleep @reconnection_delay
|
sleep @reconnection_delay
|
||||||
end
|
end
|
||||||
@@ -55,31 +54,19 @@ class W3DHub
|
|||||||
@auto_reconnect = false
|
@auto_reconnect = false
|
||||||
|
|
||||||
logger.debug(LOG_TAG) { "Requesting connection token..." }
|
logger.debug(LOG_TAG) { "Requesting connection token..." }
|
||||||
|
response = Api.post("/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, "", :gsh)
|
||||||
|
|
||||||
result = nil
|
if response.status != 200
|
||||||
Api.post("/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, "", :gsh) do |callback_result|
|
|
||||||
result = callback_result
|
|
||||||
end
|
|
||||||
|
|
||||||
# FIXME: we've introduced ourselves to callback hell, yay!
|
|
||||||
while result.nil?
|
|
||||||
sleep 0.1
|
|
||||||
end
|
|
||||||
|
|
||||||
if result.error?
|
|
||||||
@auto_reconnect = true
|
@auto_reconnect = true
|
||||||
@reconnection_delay *= 2
|
@reconnection_delay = @reconnection_delay * 2
|
||||||
@reconnection_delay = 60 if @reconnection_delay > 60
|
@reconnection_delay = 60 if @reconnection_delay > 60
|
||||||
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@reconnection_delay = 1
|
@reconnection_delay = 1
|
||||||
|
|
||||||
connect_websocket(JSON.parse(result.data, symbolize_names: true))
|
data = JSON.parse(response.body, symbolize_names: true)
|
||||||
end
|
|
||||||
|
|
||||||
def connect_websocket(data)
|
|
||||||
@invocation_id = 0 if @invocation_id > 9095
|
@invocation_id = 0 if @invocation_id > 9095
|
||||||
id = data[:connectionToken]
|
id = data[:connectionToken]
|
||||||
endpoint = "#{Api::SERVER_LIST_ENDPOINT}/listings/push/v2?id=#{id}"
|
endpoint = "#{Api::SERVER_LIST_ENDPOINT}/listings/push/v2?id=#{id}"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class W3DHub
|
|||||||
# unpack packages
|
# unpack packages
|
||||||
# install dependencies (e.g. visual C runtime)
|
# install dependencies (e.g. visual C runtime)
|
||||||
|
|
||||||
@tasks.push(Installer.new(context: task_context(app_id, channel, "version")))
|
@tasks.push(Installer.new(app_id, channel))
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(app_id, channel)
|
def update(app_id, channel)
|
||||||
@@ -31,9 +31,7 @@ class W3DHub
|
|||||||
|
|
||||||
return false unless installed?(app_id, channel)
|
return false unless installed?(app_id, channel)
|
||||||
|
|
||||||
updater = Updater.new(Installer.new(context: task_context(app_id, channel, "version")))
|
@tasks.push(Updater.new(app_id, channel))
|
||||||
|
|
||||||
@tasks.push(updater)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def import(app_id, channel)
|
def import(app_id, channel)
|
||||||
@@ -96,18 +94,6 @@ class W3DHub
|
|||||||
Process.spawn(exe)
|
Process.spawn(exe)
|
||||||
end
|
end
|
||||||
|
|
||||||
def task_context(app_id, channel, version)
|
|
||||||
Task::Context.new(
|
|
||||||
SecureRandom.hex,
|
|
||||||
"games",
|
|
||||||
app_id,
|
|
||||||
channel,
|
|
||||||
version,
|
|
||||||
"",
|
|
||||||
""
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def repair(app_id, channel)
|
def repair(app_id, channel)
|
||||||
logger.info(LOG_TAG) { "Repair Installation Request: #{app_id}-#{channel}" }
|
logger.info(LOG_TAG) { "Repair Installation Request: #{app_id}-#{channel}" }
|
||||||
|
|
||||||
@@ -120,7 +106,7 @@ class W3DHub
|
|||||||
# unpack packages
|
# unpack packages
|
||||||
# install dependencies (e.g. visual C runtime) if appropriate
|
# install dependencies (e.g. visual C runtime) if appropriate
|
||||||
|
|
||||||
@tasks.push(Repairer.new(context: task_context(app_id, channel, "version")))
|
@tasks.push(Repairer.new(app_id, channel))
|
||||||
end
|
end
|
||||||
|
|
||||||
def uninstall(app_id, channel)
|
def uninstall(app_id, channel)
|
||||||
@@ -135,7 +121,7 @@ class W3DHub
|
|||||||
title: "Uninstall #{game.name}?",
|
title: "Uninstall #{game.name}?",
|
||||||
message: "Are you sure you want to uninstall #{game.name} (#{channel})?",
|
message: "Are you sure you want to uninstall #{game.name} (#{channel})?",
|
||||||
accept_callback: proc {
|
accept_callback: proc {
|
||||||
@tasks.push(Uninstaller.new(context: task_context(app_id, channel, "version")))
|
@tasks.push(Uninstaller.new(app_id, channel))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -188,7 +174,6 @@ class W3DHub
|
|||||||
return vars if W3DHub.windows?
|
return vars if W3DHub.windows?
|
||||||
|
|
||||||
vars["WINEPREFIX"] = Store.settings[:wine_prefix] unless Store.settings[:wine_prefix].to_s.empty?
|
vars["WINEPREFIX"] = Store.settings[:wine_prefix] unless Store.settings[:wine_prefix].to_s.empty?
|
||||||
# vars["WINEDEBUG"] = "-all" if true # TODO make this an option. wine debug interferences with pid returned from Process.spawn
|
|
||||||
|
|
||||||
vars
|
vars
|
||||||
end
|
end
|
||||||
@@ -258,6 +243,7 @@ class W3DHub
|
|||||||
wine_enviroment_variables(app_id, channel)
|
wine_enviroment_variables(app_id, channel)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
attempted = false
|
attempted = false
|
||||||
begin
|
begin
|
||||||
pid = Process.spawn(
|
pid = Process.spawn(
|
||||||
@@ -269,7 +255,6 @@ class W3DHub
|
|||||||
"-launcher #{args.join(' ')}"
|
"-launcher #{args.join(' ')}"
|
||||||
)
|
)
|
||||||
Process.detach(pid)
|
Process.detach(pid)
|
||||||
BackgroundWorker.foreground_parallel_job(-> { monitor_process(app_id, channel, pid) }, ->(result) { handle_process_result(app_id, channel, result) })
|
|
||||||
rescue Errno::EINVAL => e
|
rescue Errno::EINVAL => e
|
||||||
retryable = !attempted
|
retryable = !attempted
|
||||||
attempted = true
|
attempted = true
|
||||||
@@ -282,51 +267,14 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def monitor_process(app_id, channel, pid)
|
|
||||||
key = "#{app_id}-#{channel}"
|
|
||||||
@running_applications[key] = pid
|
|
||||||
|
|
||||||
status = Process::Status.wait(pid)
|
|
||||||
pp [pid, status]
|
|
||||||
|
|
||||||
@running_applications.delete(key)
|
|
||||||
|
|
||||||
status
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_process_result(app_id, channel, status)
|
|
||||||
pp [app_id, channel, status]
|
|
||||||
|
|
||||||
# Everything's fine
|
|
||||||
return if status.pid >= 0 && status.success?
|
|
||||||
|
|
||||||
# Everything's not fine
|
|
||||||
reason = status.pid.positive? ? "Crashed" : "Failed to Launch"
|
|
||||||
game = Store.applications.games.find { |g| g.id == app_id }
|
|
||||||
title = "#{reason}: #{game.name}" if game
|
|
||||||
title = "Application #{reason}" unless game
|
|
||||||
|
|
||||||
message = if status.pid.negative?
|
|
||||||
"Command Not Found."
|
|
||||||
else
|
|
||||||
"Application crashed."
|
|
||||||
end
|
|
||||||
|
|
||||||
push_state(
|
|
||||||
States::MessageDialog,
|
|
||||||
title: title,
|
|
||||||
message: message,
|
|
||||||
accept_callback: proc {
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def join_server(app_id, channel, server, username = Store.settings[:server_list_username], password = nil, multi = false)
|
def join_server(app_id, channel, server, username = Store.settings[:server_list_username], password = nil, multi = false)
|
||||||
return unless installed?(app_id, channel) && username.to_s.length.positive?
|
return unless installed?(app_id, channel) && username.to_s.length.positive?
|
||||||
|
|
||||||
run(
|
run(
|
||||||
app_id, channel,
|
app_id, channel,
|
||||||
"+connect #{server.address}:#{server.port} +netplayername #{username}#{password ? " +password \"#{password}\"" : ""}#{multi ? " +multi" : ""}"
|
"+connect #{server.address}:#{server.port} "\
|
||||||
|
"+netplayername #{username}#{password ? " +password \"#{password}\"" : ""}"\
|
||||||
|
"#{multi ? " +multi" : ""}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -555,7 +503,7 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def installing?(app_id, channel)
|
def installing?(app_id, channel)
|
||||||
@tasks.find { |t| t.is_a?(Installer) && t.context.app_id == app_id && t.context.channel_id == channel }
|
@tasks.find { |t| t.is_a?(Installer) && t.app_id == app_id && t.release_channel == channel }
|
||||||
end
|
end
|
||||||
|
|
||||||
def updateable?(app_id, channel)
|
def updateable?(app_id, channel)
|
||||||
@@ -608,54 +556,6 @@ class W3DHub
|
|||||||
app.channels.detect { |g| g.id.to_s == channel_id.to_s }
|
app.channels.detect { |g| g.id.to_s == channel_id.to_s }
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_task_event(event)
|
|
||||||
# ONLY CALL on MAIN Ractor
|
|
||||||
raise "Something has gone horribly wrong!" unless Ractor.main?
|
|
||||||
|
|
||||||
pp event
|
|
||||||
task = @tasks.find { |t| t.context.task_id == event.task_id }
|
|
||||||
return unless task # FIXME: This is probably a fatal error
|
|
||||||
|
|
||||||
case event.type
|
|
||||||
when Task::EVENT_FAILURE
|
|
||||||
Store.main_thread_queue << proc do
|
|
||||||
window.push_state(
|
|
||||||
W3DHub::States::MessageDialog,
|
|
||||||
type: event.data[:type],
|
|
||||||
title: event.data[:title],
|
|
||||||
message: event.data[:message]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
# FIXME: Send event to Games page to trigger refresh
|
|
||||||
|
|
||||||
States::Interface.instance&.hide_application_taskbar
|
|
||||||
@tasks.delete(task)
|
|
||||||
|
|
||||||
when Task::EVENT_START
|
|
||||||
States::Interface.instance&.show_application_taskbar
|
|
||||||
|
|
||||||
when Task::EVENT_SUCCESS
|
|
||||||
States::Interface.instance&.hide_application_taskbar
|
|
||||||
@tasks.delete(task)
|
|
||||||
# FIXME: Send event to Games page to trigger refresh
|
|
||||||
when Task::EVENT_STATUS
|
|
||||||
task.status = event.data
|
|
||||||
States::Interface.instance&.update_interface_task_status(task)
|
|
||||||
|
|
||||||
when Task::EVENT_STATUS_OPERATION
|
|
||||||
hash = event.data
|
|
||||||
operation = task.status.operations[operation[:id]]
|
|
||||||
|
|
||||||
if operation
|
|
||||||
operation.label = hash[:label]
|
|
||||||
operation.value = hash[:value]
|
|
||||||
operation.progress = hash[:progress]
|
|
||||||
|
|
||||||
States::Interface.instance&.update_interface_task_status(task)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# No application tasks are being done
|
# No application tasks are being done
|
||||||
def idle?
|
def idle?
|
||||||
!busy?
|
!busy?
|
||||||
@@ -676,37 +576,15 @@ class W3DHub
|
|||||||
@tasks.delete_if { |t| t.state == :complete || t.state == :halted || t.state == :failed }
|
@tasks.delete_if { |t| t.state == :complete || t.state == :halted || t.state == :failed }
|
||||||
|
|
||||||
task = @tasks.find { |t| t.state == :not_started }
|
task = @tasks.find { |t| t.state == :not_started }
|
||||||
|
task&.start
|
||||||
return unless task
|
|
||||||
|
|
||||||
# mark MAIN ractor's task as started before handing off to background ractor
|
|
||||||
# so that we don't start up multiple tasks at once.
|
|
||||||
task.start
|
|
||||||
on_ractor(task)
|
|
||||||
end
|
|
||||||
|
|
||||||
def on_ractor(task)
|
|
||||||
raise "Something has gone horribly wrong!!!" unless Ractor.main?
|
|
||||||
|
|
||||||
ractor = Ractor.new(task) do |t|
|
|
||||||
t.start
|
|
||||||
end
|
|
||||||
|
|
||||||
Thread.new do
|
|
||||||
while (message_event = ractor.take)
|
|
||||||
break unless message_event.is_a?(Task::MessageEvent)
|
|
||||||
|
|
||||||
Store.application_manager.handle_task_event(message_event)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def task?(type, app_id, channel)
|
def task?(type, app_id, channel)
|
||||||
@tasks.find do |t|
|
@tasks.find do |t|
|
||||||
t.type == type &&
|
t.type == type &&
|
||||||
t.context.app_id == app_id &&
|
t.app_id == app_id &&
|
||||||
t.context.channel_id == channel &&
|
t.release_channel == channel &&
|
||||||
[ :not_started, :running, :paused ].include?(t.state)
|
[ :not_started, :running, :paused ].include?(t.state)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
class W3DHub
|
class W3DHub
|
||||||
class ApplicationManager
|
class ApplicationManager
|
||||||
class Status
|
class Status
|
||||||
attr_reader :application, :channel, :operations, :data
|
attr_reader :application, :channel, :step, :operations, :data
|
||||||
attr_accessor :label, :value, :progress, :step
|
attr_accessor :label, :value, :progress
|
||||||
|
|
||||||
def initialize(application:, channel:, label: "", value: "", progress: 0.0, step: :pending, operations: {})
|
def initialize(application:, channel:, label: "", value: "", progress: 0.0, step: :pending, operations: {}, &callback)
|
||||||
@application = application
|
@application = application
|
||||||
@channel = channel
|
@channel = channel
|
||||||
|
|
||||||
@@ -15,10 +15,17 @@ class W3DHub
|
|||||||
@step = step
|
@step = step
|
||||||
@operations = operations
|
@operations = operations
|
||||||
|
|
||||||
|
@callback = callback
|
||||||
|
|
||||||
@data = {}
|
@data = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def step=(sym)
|
||||||
|
@step = sym
|
||||||
|
@callback&.call(self)
|
||||||
|
@step
|
||||||
|
end
|
||||||
|
|
||||||
class Operation
|
class Operation
|
||||||
attr_accessor :label, :value, :progress
|
attr_accessor :label, :value, :progress
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,9 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def execute_task
|
def execute_task
|
||||||
fail_fast!
|
show_application_taskbar
|
||||||
|
|
||||||
|
fail_fast
|
||||||
return false if failed?
|
return false if failed?
|
||||||
|
|
||||||
fetch_manifests
|
fetch_manifests
|
||||||
@@ -44,6 +46,9 @@ class W3DHub
|
|||||||
mark_application_installed
|
mark_application_installed
|
||||||
return false if failed?
|
return false if failed?
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
hide_application_taskbar
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
166
lib/background_worker.rb
Normal file
166
lib/background_worker.rb
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
class W3DHub
|
||||||
|
class BackgroundWorker
|
||||||
|
LOG_TAG = "W3DHub::BackgroundWorker"
|
||||||
|
@@instance = nil
|
||||||
|
@@alive = false
|
||||||
|
|
||||||
|
def self.create
|
||||||
|
raise "BackgroundWorker instance already exists!" if @@instance
|
||||||
|
logger.info(LOG_TAG) { "Starting background job worker..." }
|
||||||
|
|
||||||
|
|
||||||
|
@@thread = Thread.current
|
||||||
|
@@alive = true
|
||||||
|
@@run = true
|
||||||
|
@@instance = self.new
|
||||||
|
|
||||||
|
@@instance.handle_jobs
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.instance
|
||||||
|
@@instance
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.run?
|
||||||
|
@@run
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.alive?
|
||||||
|
@@alive
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.busy?
|
||||||
|
instance&.busy?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.shutdown!
|
||||||
|
@@run = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.kill!
|
||||||
|
@@thread.kill
|
||||||
|
|
||||||
|
@@instance.kill!
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.job(job, callback, error_handler = nil, data = nil)
|
||||||
|
@@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler, data: data))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.parallel_job(job, callback, error_handler = nil, data = nil)
|
||||||
|
@@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler, data: data))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.foreground_job(job, callback, error_handler = nil, data = nil)
|
||||||
|
@@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true, data: data))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.foreground_parallel_job(job, callback, error_handler = nil, data = nil)
|
||||||
|
@@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true, data: data))
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@busy = false
|
||||||
|
@jobs = []
|
||||||
|
|
||||||
|
# Jobs which are order independent
|
||||||
|
@parallel_busy = false
|
||||||
|
@thread_pool = []
|
||||||
|
@parallel_jobs = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def kill!
|
||||||
|
@thread_pool.each(&:kill)
|
||||||
|
|
||||||
|
logger.info(LOG_TAG) { "Forcefully killed background job worker." }
|
||||||
|
@@alive = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_jobs
|
||||||
|
8.times do |i|
|
||||||
|
Thread.new do
|
||||||
|
@thread_pool << Thread.current
|
||||||
|
|
||||||
|
while BackgroundWorker.run?
|
||||||
|
job = @parallel_jobs.shift
|
||||||
|
|
||||||
|
@parallel_busy = true
|
||||||
|
|
||||||
|
begin
|
||||||
|
job&.do
|
||||||
|
rescue => e
|
||||||
|
job&.raise_error(e)
|
||||||
|
end
|
||||||
|
|
||||||
|
@parallel_busy = !@parallel_jobs.empty?
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Thread.new do
|
||||||
|
@thread_pool << Thread.current
|
||||||
|
|
||||||
|
while BackgroundWorker.run?
|
||||||
|
job = @jobs.shift
|
||||||
|
|
||||||
|
@busy = true
|
||||||
|
|
||||||
|
begin
|
||||||
|
job&.do
|
||||||
|
rescue => e
|
||||||
|
job&.raise_error(e)
|
||||||
|
end
|
||||||
|
|
||||||
|
@busy = !@jobs.empty?
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
|
||||||
|
logger.info(LOG_TAG) { "Stopped background job worker." }
|
||||||
|
@@alive = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_job(job)
|
||||||
|
@jobs << job
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_parallel_job(job)
|
||||||
|
@parallel_jobs << job
|
||||||
|
end
|
||||||
|
|
||||||
|
def busy?
|
||||||
|
@busy || @parallel_busy
|
||||||
|
end
|
||||||
|
|
||||||
|
class Job
|
||||||
|
def initialize(job:, callback:, error_handler: nil, deliver_to_queue: false, data: nil)
|
||||||
|
@job = job
|
||||||
|
@callback = callback
|
||||||
|
@error_handler = error_handler
|
||||||
|
@deliver_to_queue = deliver_to_queue
|
||||||
|
@data = data
|
||||||
|
end
|
||||||
|
|
||||||
|
def do
|
||||||
|
result = @data ? @job.call(@data) : @job.call
|
||||||
|
deliver(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
def deliver(result)
|
||||||
|
if @deliver_to_queue
|
||||||
|
Store.main_thread_queue << -> { @callback.call(result) }
|
||||||
|
else
|
||||||
|
@callback.call(result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def raise_error(error)
|
||||||
|
logger.error error
|
||||||
|
@error_handler&.call(error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -90,6 +90,8 @@ class W3DHub
|
|||||||
result = true
|
result = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
binding.irb unless response
|
||||||
|
|
||||||
if response&.status == 200 || response&.status == 206
|
if response&.status == 200 || response&.status == 206
|
||||||
result = true
|
result = true
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
class W3DHub
|
class W3DHub
|
||||||
PLATFORM_WINDOWS = RbConfig::CONFIG["host_os"] =~ /(mingw|mswin|windows)/i
|
|
||||||
PLATFORM_DARWIN = RbConfig::CONFIG["host_os"] =~ /(darwin|mac os)/i
|
|
||||||
PLATFORM_LINUX = RbConfig::CONFIG["host_os"] =~ /(linux|bsd|aix|solaris)/i
|
|
||||||
|
|
||||||
def self.format_size(bytes)
|
def self.format_size(bytes)
|
||||||
case bytes
|
case bytes
|
||||||
when 0..1023 # Bytes
|
when 0..1023 # Bytes
|
||||||
@@ -21,15 +17,15 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.windows?
|
def self.windows?
|
||||||
PLATFORM_WINDOWS
|
RbConfig::CONFIG["host_os"] =~ /(mingw|mswin|windows)/i
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.mac?
|
def self.mac?
|
||||||
PLATFORM_DARWIN
|
RbConfig::CONFIG["host_os"] =~ /(darwin|mac os)/i
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.linux?
|
def self.linux?
|
||||||
PLATFORM_LINUX
|
RbConfig::CONFIG["host_os"] =~ /(linux|bsd|aix|solaris)/i
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.unix?
|
def self.unix?
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
class W3DHub
|
|
||||||
# all http(s) requests for API calls and downloading images run through here
|
|
||||||
class NetworkManager
|
|
||||||
NetworkEvent = Data.define(:context, :result)
|
|
||||||
Request = Struct.new(:context, :callback)
|
|
||||||
Context = Data.define(
|
|
||||||
:request_id,
|
|
||||||
:method,
|
|
||||||
:url,
|
|
||||||
:headers,
|
|
||||||
:body,
|
|
||||||
:bearer_token
|
|
||||||
)
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@requests = []
|
|
||||||
@running = true
|
|
||||||
|
|
||||||
Thread.new do
|
|
||||||
http_client = HttpClient.new
|
|
||||||
|
|
||||||
Sync do
|
|
||||||
while @running
|
|
||||||
request = @requests.shift
|
|
||||||
|
|
||||||
# goto sleep for an second if there is no work to be doing
|
|
||||||
unless request
|
|
||||||
sleep 1
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
Async do |task|
|
|
||||||
assigned_request = request
|
|
||||||
result = http_client.handle(task, assigned_request)
|
|
||||||
|
|
||||||
pp [assigned_request, result]
|
|
||||||
|
|
||||||
Store.main_thread_queue << -> { assigned_request.callback.call(result) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def request(method, url, headers, body, bearer_token, &block)
|
|
||||||
request_id = SecureRandom.hex
|
|
||||||
|
|
||||||
request = Request.new(
|
|
||||||
Context.new(
|
|
||||||
request_id,
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
bearer_token
|
|
||||||
),
|
|
||||||
block
|
|
||||||
)
|
|
||||||
|
|
||||||
@requests << request
|
|
||||||
|
|
||||||
request_id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
class W3DHub
|
|
||||||
class NetworkManager
|
|
||||||
# non-blocking, http requests.
|
|
||||||
class HttpClient
|
|
||||||
def initialize
|
|
||||||
@http_clients = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle(task, request)
|
|
||||||
result = CyberarmEngine::Result.new
|
|
||||||
context = request.context
|
|
||||||
|
|
||||||
task.with_timeout(30) do
|
|
||||||
uri = URI(context.url)
|
|
||||||
|
|
||||||
pp uri
|
|
||||||
|
|
||||||
response = provision_http_client(uri.origin).send(
|
|
||||||
context.method,
|
|
||||||
uri.path,
|
|
||||||
context.headers,
|
|
||||||
context.body
|
|
||||||
)
|
|
||||||
|
|
||||||
pp response
|
|
||||||
|
|
||||||
if response.success?
|
|
||||||
result.data = response.body.read
|
|
||||||
else
|
|
||||||
result.error = response
|
|
||||||
end
|
|
||||||
rescue Async::TimeoutError => e
|
|
||||||
result.error = e
|
|
||||||
rescue StandardError => e
|
|
||||||
result.error = e
|
|
||||||
ensure
|
|
||||||
response&.close
|
|
||||||
end
|
|
||||||
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def provision_http_client(hostname)
|
|
||||||
return @http_clients[hostname.downcase] if @http_clients[hostname.downcase]
|
|
||||||
|
|
||||||
ssl_context = W3DHub.ca_bundle_path ? OpenSSL::SSL::SSLContext.new : nil
|
|
||||||
ssl_context&.set_params(
|
|
||||||
ca_file: W3DHub.ca_bundle_path,
|
|
||||||
verify_mode: OpenSSL::SSL::VERIFY_PEER
|
|
||||||
)
|
|
||||||
|
|
||||||
endpoint = Async::HTTP::Endpoint.parse(hostname, ssl_context: ssl_context)
|
|
||||||
@http_clients[hostname.downcase] = Async::HTTP::Client.new(endpoint)
|
|
||||||
end
|
|
||||||
|
|
||||||
def wrapped_error(error)
|
|
||||||
WrappedError.new(error.class, error.message.to_s, error.backtrace)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -196,13 +196,13 @@ class W3DHub
|
|||||||
|
|
||||||
def ping_icon(server)
|
def ping_icon(server)
|
||||||
case server.ping
|
case server.ping
|
||||||
when 0..50
|
when 0..150
|
||||||
@ping_icons[:good]
|
@ping_icons[:good]
|
||||||
when 51..150
|
|
||||||
@ping_icons[:fair]
|
|
||||||
when 151..200
|
when 151..200
|
||||||
@ping_icons[:poor]
|
@ping_icons[:fair]
|
||||||
when 201..1_000
|
when 201..1_000
|
||||||
|
@ping_icons[:poor]
|
||||||
|
when 1_001..5_000
|
||||||
@ping_icons[:bad]
|
@ping_icons[:bad]
|
||||||
else
|
else
|
||||||
@ping_icons[:unknown]
|
@ping_icons[:unknown]
|
||||||
|
|||||||
166
lib/ping.rb
166
lib/ping.rb
@@ -1,166 +0,0 @@
|
|||||||
require "async"
|
|
||||||
require "socket"
|
|
||||||
require "securerandom"
|
|
||||||
|
|
||||||
class W3DHub
|
|
||||||
class Ping
|
|
||||||
ICMPHeader = Data.define(:type, :code, :checksum, :_ping_id, :_sequence_id, :data)
|
|
||||||
EchoRequest = Struct.new(:ping_id, :sequence_id, :data, :time, :timed_out)
|
|
||||||
|
|
||||||
ICMP_ECHOREPLY = 0
|
|
||||||
ICMP_ECHO = 8
|
|
||||||
ICMP_SUBCODE = 0
|
|
||||||
|
|
||||||
BIT_PACKER = "C2 n3 A*".freeze
|
|
||||||
MINIMUM_INTERVAL = 250 # ms # intervals below 200ms are considered rude and may be dropped due to flooding.
|
|
||||||
ECHO_REQUEST_HISTORY = 30 # 100 # keep the last n requests
|
|
||||||
|
|
||||||
attr_reader :address
|
|
||||||
|
|
||||||
def initialize(address:, count: 10, ttl: 120, interval: 1_000, data: nil)
|
|
||||||
@address = address
|
|
||||||
@count = count
|
|
||||||
@ttl = ttl
|
|
||||||
@interval = interval.to_i < MINIMUM_INTERVAL ? MINIMUM_INTERVAL : interval # ms
|
|
||||||
@data = data
|
|
||||||
|
|
||||||
# circular buffer
|
|
||||||
@echo_requests = Array.new(ECHO_REQUEST_HISTORY) { EchoRequest.new(-1, -1, "", nil, false) }
|
|
||||||
@echo_requests_index = 0
|
|
||||||
|
|
||||||
# NOTE: The PING_ID _might_ be overruled by the kernel and should not be used
|
|
||||||
# to check that any received echo replies are ours.
|
|
||||||
#
|
|
||||||
# Sequence ID and Data appear to be unmodified.
|
|
||||||
@ping_id = SecureRandom.hex.to_i(16) & 0xffff
|
|
||||||
@sequence_id = SecureRandom.hex.to_i(16) & 0xffff
|
|
||||||
|
|
||||||
addresses = Addrinfo.getaddrinfo(@address, nil, Socket::AF_INET, :DGRAM)
|
|
||||||
raise "NO ADDRESSES!" if addresses.empty?
|
|
||||||
|
|
||||||
@socket_address = addresses.sample.to_sockaddr
|
|
||||||
|
|
||||||
@socket = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, Socket::IPPROTO_ICMP)
|
|
||||||
@socket.setsockopt(Socket::SOL_SOCKET, Socket::IP_TTL, @ttl)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Perform a checksum on the message. This is the sum of all the short
|
|
||||||
# words and it folds the high order bits into the low order bits.
|
|
||||||
def message_checksum(message)
|
|
||||||
length = message.length
|
|
||||||
num_short = length / 2
|
|
||||||
check = 0
|
|
||||||
|
|
||||||
message.unpack("n#{num_short}").each do |short|
|
|
||||||
check += short
|
|
||||||
end
|
|
||||||
|
|
||||||
check += message[length - 1, 1].unpack1("C") << 8 if (length % 2).positive?
|
|
||||||
|
|
||||||
check = (check >> 16) + (check & 0xffff)
|
|
||||||
~((check >> 16) + check) & 0xffff
|
|
||||||
end
|
|
||||||
|
|
||||||
def random_data
|
|
||||||
SecureRandom.hex
|
|
||||||
end
|
|
||||||
|
|
||||||
def monotonic_time
|
|
||||||
Process.clock_gettime(:CLOCK_MONOTONIC, :millisecond)
|
|
||||||
end
|
|
||||||
|
|
||||||
def verified?(message)
|
|
||||||
data = message.unpack(BIT_PACKER)
|
|
||||||
checksum = data[2]
|
|
||||||
|
|
||||||
# set checksum in message to 0
|
|
||||||
data[2] = 0
|
|
||||||
|
|
||||||
checksum == message_checksum(data.pack(BIT_PACKER))
|
|
||||||
end
|
|
||||||
|
|
||||||
def request_complete?(request)
|
|
||||||
request.timed_out || !request.time.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
def packet_loss
|
|
||||||
completed_requests = @echo_requests.select { |r| request_complete?(r) }
|
|
||||||
failed_requests = completed_requests.select(&:timed_out)
|
|
||||||
|
|
||||||
# 0% packet loss 😎
|
|
||||||
return 0.0 if failed_requests.empty?
|
|
||||||
|
|
||||||
# 100% packet loss
|
|
||||||
return 1.0 if failed_requests.size == completed_requests.size
|
|
||||||
|
|
||||||
failed_requests.size / completed_requests.size.to_f
|
|
||||||
end
|
|
||||||
|
|
||||||
def average_ping
|
|
||||||
times = @echo_requests.select { |r| request_complete?(r) && !r.timed_out }.map(&:time)
|
|
||||||
|
|
||||||
return -1 unless times.size.positive?
|
|
||||||
|
|
||||||
times.sum.to_f / times.size
|
|
||||||
end
|
|
||||||
|
|
||||||
# returns true if any echo requests have completed (reply received or timed out) and packet loss is less than 30%
|
|
||||||
def okay?
|
|
||||||
completed_requests = @echo_requests.select { |r| request_complete?(r) }.size
|
|
||||||
|
|
||||||
completed_requests.positive? && packet_loss < 0.3
|
|
||||||
end
|
|
||||||
|
|
||||||
def ping(count = @count)
|
|
||||||
return if count <= 0
|
|
||||||
|
|
||||||
Async do |task|
|
|
||||||
@count.times do
|
|
||||||
task.Async do |subtask|
|
|
||||||
@sequence_id = (@sequence_id + 1) % 0xffff
|
|
||||||
data = @data || random_data
|
|
||||||
|
|
||||||
checksum = 0
|
|
||||||
message = [ICMP_ECHO, ICMP_SUBCODE, checksum, @ping_id, @sequence_id, data].pack(BIT_PACKER)
|
|
||||||
checksum = message_checksum(message)
|
|
||||||
message = [ICMP_ECHO, ICMP_SUBCODE, checksum, @ping_id, @sequence_id, data].pack(BIT_PACKER)
|
|
||||||
|
|
||||||
@socket.send(message, 0, @socket_address)
|
|
||||||
|
|
||||||
s = monotonic_time
|
|
||||||
request = @echo_requests[@echo_requests_index]
|
|
||||||
request.ping_id = @ping_id
|
|
||||||
request.sequence_id = @sequence_id
|
|
||||||
request.data = data
|
|
||||||
request.time = nil
|
|
||||||
request.timed_out = false
|
|
||||||
@echo_requests_index = (@echo_requests_index + 1) % ECHO_REQUEST_HISTORY
|
|
||||||
|
|
||||||
subtask.with_timeout(2) do
|
|
||||||
loop do
|
|
||||||
data, _addrinfo = @socket.recvfrom(1500)
|
|
||||||
|
|
||||||
# ignore corruption
|
|
||||||
next unless verified?(data)
|
|
||||||
|
|
||||||
header = ICMPHeader.new(*data.unpack(BIT_PACKER))
|
|
||||||
|
|
||||||
if header.type == ICMP_ECHOREPLY && header._sequence_id == request.sequence_id && header.data == request.data
|
|
||||||
duration = monotonic_time - s
|
|
||||||
request.time = duration
|
|
||||||
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue Async::TimeoutError
|
|
||||||
request.timed_out = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Don't send out pings in a flood, it's considered rude.
|
|
||||||
sleep @interval / 1000.0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
class W3DHub
|
|
||||||
class PingManager
|
|
||||||
Container = Struct.new(:pinger, :last_ping_time_ms)
|
|
||||||
PING_INTERVAL = 60_000
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@containers = {}
|
|
||||||
@addresses = []
|
|
||||||
end
|
|
||||||
|
|
||||||
def monitor(task)
|
|
||||||
task.async do |subtask|
|
|
||||||
while BackgroundWorker.alive?
|
|
||||||
# activate new addresses
|
|
||||||
@addresses.each do |address|
|
|
||||||
@containers[address] ||= Container.new(Ping.new(address: address), -PING_INTERVAL * 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
# cleanup old addresses
|
|
||||||
@containers.each_key do |key|
|
|
||||||
@containers.delete(key) unless @addresses.find { |a| a == key }
|
|
||||||
end
|
|
||||||
|
|
||||||
# ping the pingers
|
|
||||||
@containers.each_value do |container|
|
|
||||||
next unless Gosu.milliseconds - container.last_ping_time_ms >= PING_INTERVAL
|
|
||||||
|
|
||||||
container.last_ping_time_ms = Gosu.milliseconds
|
|
||||||
|
|
||||||
subtask.async do
|
|
||||||
container.pinger.ping
|
|
||||||
# pp [container.pinger.address, container.pinger.average_ping]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
sleep 0.001
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_address(address)
|
|
||||||
@addresses << address
|
|
||||||
@addresses.uniq!
|
|
||||||
end
|
|
||||||
|
|
||||||
def ping_for(address)
|
|
||||||
@containers[address]&.pinger&.average_ping&.round || -1
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_address(address)
|
|
||||||
@addresses.delete(address)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -175,11 +175,7 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def service_status
|
def service_status
|
||||||
@status_label.value = "Checking service status..." #I18n.t(:"server_browser.fetching_server_list")
|
|
||||||
|
|
||||||
Api.on_thread(:service_status) do |service_status|
|
Api.on_thread(:service_status) do |service_status|
|
||||||
pp service_status
|
|
||||||
|
|
||||||
@service_status = service_status
|
@service_status = service_status
|
||||||
|
|
||||||
if @service_status
|
if @service_status
|
||||||
@@ -231,14 +227,14 @@ class W3DHub
|
|||||||
def applications
|
def applications
|
||||||
@status_label.value = I18n.t(:"boot.checking_for_updates")
|
@status_label.value = I18n.t(:"boot.checking_for_updates")
|
||||||
|
|
||||||
# Api.on_thread(:_applications) do |applications|
|
Api.on_thread(:_applications) do |applications|
|
||||||
Api.on_thread(:applications, :alt_w3dhub) do |applications|
|
|
||||||
if applications
|
if applications
|
||||||
Store.applications = applications
|
Store.applications = applications
|
||||||
Store.settings.save_application_cache(applications.data.to_json)
|
Store.settings.save_application_cache(applications.data.to_json)
|
||||||
@tasks[:applications][:complete] = true
|
@tasks[:applications][:complete] = true
|
||||||
else
|
else
|
||||||
@status_label.value = "FAILED TO RETREIVE APPS LIST"
|
# FIXME: Failed to retreive!
|
||||||
|
BackgroundWorker.foreground_job(-> {}, ->(_) { @status_label.value = "FAILED TO RETREIVE APPS LIST" })
|
||||||
|
|
||||||
@offline_mode = true
|
@offline_mode = true
|
||||||
Store.offline_mode = true
|
Store.offline_mode = true
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class W3DHub
|
class W3DHub
|
||||||
DIR_NAME = "W3DHubAlt".freeze
|
DIR_NAME = "W3DHubAlt".freeze
|
||||||
VERSION = "0.9.0".freeze
|
VERSION = "0.9.2".freeze
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ class W3DHub
|
|||||||
|
|
||||||
Store[:server_list] = []
|
Store[:server_list] = []
|
||||||
Store[:settings] = Settings.new
|
Store[:settings] = Settings.new
|
||||||
Store[:network_manager] = NetworkManager.new
|
|
||||||
Store[:application_manager] = ApplicationManager.new
|
Store[:application_manager] = ApplicationManager.new
|
||||||
Store[:ping_manager] = PingManager.new
|
|
||||||
|
|
||||||
# FIXME
|
|
||||||
# BackgroundWorker.parallel_job(-> { Async { |task| Store.ping_manager.monitor(task) } }, nil)
|
|
||||||
|
|
||||||
Store[:main_thread_queue] = []
|
Store[:main_thread_queue] = []
|
||||||
|
|
||||||
@@ -36,6 +31,9 @@ class W3DHub
|
|||||||
while (block = Store.main_thread_queue.shift)
|
while (block = Store.main_thread_queue.shift)
|
||||||
block&.call
|
block&.call
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Manually sleep main thread so that the BackgroundWorker thread can be scheduled
|
||||||
|
sleep(update_interval / 1000.0) if W3DHub::BackgroundWorker.busy? || Store.application_manager.busy?
|
||||||
end
|
end
|
||||||
|
|
||||||
def needs_redraw?
|
def needs_redraw?
|
||||||
|
|||||||
@@ -228,27 +228,34 @@ class W3DHub
|
|||||||
@encrypted
|
@encrypted
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_file(path:)
|
def add_file(path:, replace: false)
|
||||||
return false unless File.exist?(path)
|
return false unless File.exist?(path)
|
||||||
return false if File.directory?(path)
|
return false if File.directory?(path)
|
||||||
|
|
||||||
info = EntryInfoHeader.new(0, 0, File.size(path))
|
entry = Entry.new(name: File.basename(path), path: path, info: EntryInfoHeader.new(0, 0, File.size(path)))
|
||||||
@entries << Entry.new(name: File.basename(path), path: path, info: info)
|
add_entry(entry: entry, replace: replace)
|
||||||
|
|
||||||
true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_blob(path:, blob:)
|
def add_blob(path:, blob:, replace: false)
|
||||||
info = EntryInfoHeader.new(0, 0, blob.size)
|
info = EntryInfoHeader.new(0, 0, blob.size)
|
||||||
@entries << Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
|
entry = Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
|
||||||
into.crc32 = @entries.last.calculate_crc32
|
into.crc32 = @entries.last.calculate_crc32
|
||||||
|
|
||||||
true
|
add_entry(entry: entry, replace: replace)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_entry(entry:)
|
def add_entry(entry:, replace: false)
|
||||||
@entries << entry
|
duplicate = @entries.find { |e| e.name.upcase == entry.name.upcase }
|
||||||
|
|
||||||
|
if duplicate
|
||||||
|
if replace
|
||||||
|
@entries.delete(duplicate)
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@entries << entry
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -106,14 +106,11 @@ require_relative "lib/cache"
|
|||||||
require_relative "lib/settings"
|
require_relative "lib/settings"
|
||||||
require_relative "lib/ww_mix"
|
require_relative "lib/ww_mix"
|
||||||
require_relative "lib/ico"
|
require_relative "lib/ico"
|
||||||
require_relative "lib/ping"
|
|
||||||
require_relative "lib/ping_manager"
|
|
||||||
require_relative "lib/broadcast_server"
|
require_relative "lib/broadcast_server"
|
||||||
require_relative "lib/hardware_survey"
|
require_relative "lib/hardware_survey"
|
||||||
require_relative "lib/game_settings"
|
require_relative "lib/game_settings"
|
||||||
require_relative "lib/websocket_client"
|
require_relative "lib/websocket_client"
|
||||||
require_relative "lib/network_manager"
|
require_relative "lib/background_worker"
|
||||||
require_relative "lib/network_manager/http_client"
|
|
||||||
require_relative "lib/application_manager"
|
require_relative "lib/application_manager"
|
||||||
require_relative "lib/application_manager/manifest"
|
require_relative "lib/application_manager/manifest"
|
||||||
require_relative "lib/application_manager/status"
|
require_relative "lib/application_manager/status"
|
||||||
@@ -179,18 +176,27 @@ end
|
|||||||
|
|
||||||
logger.info(W3DHub::LOG_TAG) { "W3D Hub Linux Launcher v#{W3DHub::VERSION}" }
|
logger.info(W3DHub::LOG_TAG) { "W3D Hub Linux Launcher v#{W3DHub::VERSION}" }
|
||||||
|
|
||||||
|
Thread.new do
|
||||||
|
W3DHub::BackgroundWorker.create
|
||||||
|
end
|
||||||
|
|
||||||
|
until W3DHub::BackgroundWorker.alive?
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
|
||||||
logger.info(W3DHub::LOG_TAG) { "Launching window..." }
|
logger.info(W3DHub::LOG_TAG) { "Launching window..." }
|
||||||
# W3DHub::Window.new(width: 980, height: 720, borderless: false, resizable: true).show unless defined?(Ocra)
|
# W3DHub::Window.new(width: 980, height: 720, borderless: false, resizable: true).show unless defined?(Ocra)
|
||||||
W3DHub::Window.new(width: 1280, height: 800, borderless: false, resizable: true).show unless defined?(Ocra)
|
W3DHub::Window.new(width: 1280, height: 800, borderless: false, resizable: true).show unless defined?(Ocra)
|
||||||
# W3DHub::Window.new(width: 1920, height: 1080, borderless: false, resizable: true).show unless defined?(Ocra)
|
# W3DHub::Window.new(width: 1920, height: 1080, borderless: false, resizable: true).show unless defined?(Ocra)
|
||||||
|
W3DHub::BackgroundWorker.shutdown!
|
||||||
|
|
||||||
# worker_soft_halt = Gosu.milliseconds
|
worker_soft_halt = Gosu.milliseconds
|
||||||
|
|
||||||
# # Wait for BackgroundWorker to return
|
# Wait for BackgroundWorker to return
|
||||||
# while W3DHub::BackgroundWorker.alive?
|
while W3DHub::BackgroundWorker.alive?
|
||||||
# W3DHub::BackgroundWorker.kill! if Gosu.milliseconds - worker_soft_halt >= 1_000
|
W3DHub::BackgroundWorker.kill! if Gosu.milliseconds - worker_soft_halt >= 1_000
|
||||||
|
|
||||||
# sleep 0.1
|
sleep 0.1
|
||||||
# end
|
end
|
||||||
|
|
||||||
W3DHub::LOGGER&.close
|
W3DHub::LOGGER&.close
|
||||||
|
|||||||
Reference in New Issue
Block a user