Move status refresh intervals to App component (resolves #107).

This commit is contained in:
Daniel Scalzi 2020-09-13 18:43:01 -04:00
parent 67e42ead78
commit 0cbd39b79c
No known key found for this signature in database
GPG Key ID: D18EA3FB4B142A57
10 changed files with 1911 additions and 816 deletions

2344
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -31,14 +31,14 @@
"dependencies": {
"adm-zip": "^0.4.16",
"async": "^3.2.0",
"discord-rpc": "^3.1.3",
"discord-rpc": "^3.1.4",
"electron-updater": "^4.3.4",
"fs-extra": "^9.0.1",
"github-syntax-dark": "^0.5.0",
"got": "^11.6.0",
"got": "^11.6.2",
"jquery": "^3.5.1",
"lodash": "^4.17.20",
"moment": "^2.27.0",
"moment": "^2.28.0",
"request": "^2.88.2",
"semver": "^7.3.2",
"tar-fs": "^2.1.0",
@ -58,7 +58,7 @@
"@types/jquery": "^3.5.1",
"@types/lodash": "^4.14.161",
"@types/mocha": "^8.0.3",
"@types/node": "^12.12.55",
"@types/node": "^12.12.58",
"@types/react": "^16.9.49",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9",
@ -67,19 +67,19 @@
"@types/tar-fs": "^2.0.0",
"@types/triple-beam": "^1.3.2",
"@types/winreg": "^1.2.30",
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"cross-env": "^7.0.2",
"electron": "^9.2.1",
"electron": "^9.3.0",
"electron-builder": "^22.8.0",
"electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2",
"electron-webpack-ts": "^4.0.1",
"eslint": "^7.8.1",
"eslint": "^7.9.0",
"eslint-plugin-react": "^7.20.6",
"helios-distribution-types": "1.0.0-pre.1",
"helios-distribution-types": "1.0.0-rc.1",
"mocha": "^8.1.3",
"nock": "^13.0.4",
"react": "^16.13.0",

View File

@ -29,6 +29,7 @@ export interface ServerStatus {
version: string
}[]
}
retrievedAt: number // Internal tracking
}
/**
@ -77,6 +78,7 @@ function unifyStatusResponse(resp: ServerStatus): ServerStatus {
text: resp.description
}
}
resp.retrievedAt = (new Date()).getTime()
return resp
}

View File

@ -30,44 +30,48 @@ export class MojangRestAPI {
version: 1
}
protected static statuses: MojangStatus[] = [
{
service: 'sessionserver.mojang.com',
status: MojangStatusColor.GREY,
name: 'Multiplayer Session Service',
essential: true
},
{
service: 'authserver.mojang.com',
status: MojangStatusColor.GREY,
name: 'Authentication Service',
essential: true
},
{
service: 'textures.minecraft.net',
status: MojangStatusColor.GREY,
name: 'Minecraft Skins',
essential: false
},
{
service: 'api.mojang.com',
status: MojangStatusColor.GREY,
name: 'Public API',
essential: false
},
{
service: 'minecraft.net',
status: MojangStatusColor.GREY,
name: 'Minecraft.net',
essential: false
},
{
service: 'account.mojang.com',
status: MojangStatusColor.GREY,
name: 'Mojang Accounts Website',
essential: false
}
]
protected static statuses: MojangStatus[] = MojangRestAPI.getDefaultStatuses()
public static getDefaultStatuses(): MojangStatus[] {
return [
{
service: 'sessionserver.mojang.com',
status: MojangStatusColor.GREY,
name: 'Multiplayer Session Service',
essential: true
},
{
service: 'authserver.mojang.com',
status: MojangStatusColor.GREY,
name: 'Authentication Service',
essential: true
},
{
service: 'textures.minecraft.net',
status: MojangStatusColor.GREY,
name: 'Minecraft Skins',
essential: false
},
{
service: 'api.mojang.com',
status: MojangStatusColor.GREY,
name: 'Public API',
essential: false
},
{
service: 'minecraft.net',
status: MojangStatusColor.GREY,
name: 'Minecraft.net',
essential: false
},
{
service: 'account.mojang.com',
status: MojangStatusColor.GREY,
name: 'Mojang Accounts Website',
essential: false
}
]
}
/**
* Converts a Mojang status color to a hex value. Valid statuses

View File

@ -24,6 +24,10 @@ import { DistributionAPI } from 'common/distribution/DistributionAPI'
import { getServerStatus, ServerStatus } from 'common/mojang/net/ServerStatusAPI'
import { Distribution } from 'helios-distribution-types'
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse'
import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus'
import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI'
import { RestResponseStatus } from 'common/got/RestResponse'
import './Application.css'
@ -39,8 +43,9 @@ interface ApplicationProps {
currentView: View
overlayQueue: OverlayPushAction<unknown>[]
distribution: HeliosDistribution
selectedServer: HeliosServer
selectedServerStatus: ServerStatus
selectedServer?: HeliosServer
selectedServerStatus?: ServerStatus
mojangStatuses: MojangStatus[]
}
interface ApplicationState {
@ -54,8 +59,9 @@ const mapState = (state: StoreType): Partial<ApplicationProps> => {
return {
currentView: state.currentView,
overlayQueue: state.overlayQueue,
distribution: state.app.distribution!,
selectedServer: state.app.selectedServer!
distribution: state.app.distribution,
selectedServer: state.app.selectedServer,
mojangStatuses: state.app.mojangStatuses
}
}
const mapDispatch = {
@ -64,13 +70,18 @@ const mapDispatch = {
...OverlayActionDispatch
}
class Application extends React.Component<ApplicationProps & typeof mapDispatch, ApplicationState> {
type InternalApplicationProps = ApplicationProps & typeof mapDispatch
class Application extends React.Component<InternalApplicationProps, ApplicationState> {
private static readonly logger = LoggerUtil.getLogger('ApplicationTSX')
private mojangStatusInterval!: NodeJS.Timeout
private serverStatusInterval!: NodeJS.Timeout
private bkid!: number
constructor(props: ApplicationProps & typeof mapDispatch) {
constructor(props: InternalApplicationProps) {
super(props)
this.state = {
loading: true,
@ -80,6 +91,83 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
}
}
async componentDidMount(): Promise<void> {
this.mojangStatusInterval = setInterval(async () => {
Application.logger.info('Refreshing Mojang Statuses..')
await this.loadMojangStatuses()
}, 300000)
this.serverStatusInterval = setInterval(async () => {
Application.logger.info('Refreshing selected server status..')
await this.syncServerStatus()
}, 300000)
}
componentWillUnmount(): void {
// Clean up intervals.
clearInterval(this.mojangStatusInterval)
clearInterval(this.serverStatusInterval)
}
async componentDidUpdate(prevProps: InternalApplicationProps): Promise<void> {
if(this.props.selectedServer?.rawServer.id !== prevProps.selectedServer?.rawServer.id) {
await this.syncServerStatus()
}
}
/**
* Load the mojang statuses and add them to the global store.
*/
private loadMojangStatuses = async (): Promise<void> => {
const response: MojangResponse<MojangStatus[]> = await MojangRestAPI.status()
if(response.responseStatus !== RestResponseStatus.SUCCESS) {
Application.logger.warn('Failed to retrieve Mojang Statuses.')
}
// TODO Temp workaround because their status checker always shows
// this as red. https://bugs.mojang.com/browse/WEB-2303
const statuses = response.data
for(const status of statuses) {
if(status.service === 'sessionserver.mojang.com' || status.service === 'minecraft.net') {
status.status = MojangStatusColor.GREEN
}
}
this.props.setMojangStatuses(response.data)
}
/**
* Fetch the status of the selected server and store it in the global store.
*/
private syncServerStatus = async (): Promise<void> => {
let serverStatus: ServerStatus | undefined
if(this.props.selectedServer != null) {
const { hostname, port } = this.props.selectedServer
try {
serverStatus = await getServerStatus(
47,
hostname,
port
)
} catch(err) {
Application.logger.error('Error while refreshing server status', err)
}
} else {
serverStatus = undefined
}
this.props.setSelectedServerStatus(serverStatus)
}
private getViewElement = (): JSX.Element => {
// TODO debug remove
console.log('loading', this.props.currentView, this.state.workingView)
@ -94,6 +182,7 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
distribution={this.props.distribution}
selectedServer={this.props.selectedServer}
selectedServerStatus={this.props.selectedServerStatus}
mojangStatuses={this.props.mojangStatuses}
/>
</>
case View.LOGIN:
@ -184,6 +273,10 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
this.props.setSelectedServerStatus(selectedServerStatus)
}
// Load initial mojang statuses.
Application.logger.info('Loading mojang statuses..')
await this.loadMojangStatuses()
// TODO Setup hook for distro refresh every ~ 5 mins.
// Pick a background id.

View File

@ -6,11 +6,9 @@ import { StoreType } from '../../redux/store'
import { AppActionDispatch } from '../..//redux/actions/appActions'
import { OverlayActionDispatch } from '../../redux/actions/overlayActions'
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
import { ServerStatus, getServerStatus } from 'common/mojang/net/ServerStatusAPI'
import { ServerStatus } from 'common/mojang/net/ServerStatusAPI'
import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus'
import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse'
import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI'
import { RestResponseStatus } from 'common/got/RestResponse'
import { LoggerUtil } from 'common/logging/loggerutil'
import News from '../news/News'
@ -19,20 +17,21 @@ import './Landing.css'
interface LandingProps {
distribution: HeliosDistribution
selectedServer: HeliosServer
selectedServerStatus: ServerStatus
selectedServer?: HeliosServer
selectedServerStatus?: ServerStatus
mojangStatuses: MojangStatus[]
}
interface LandingState {
mojangStatuses: MojangStatus[]
outdatedServerStatus: boolean
workingServerStatus?: ServerStatus
}
const mapState = (state: StoreType): Partial<LandingProps> => {
return {
distribution: state.app.distribution!,
selectedServer: state.app.selectedServer!,
selectedServerStatus: state.app.selectedServerStatus!
selectedServer: state.app.selectedServer,
selectedServerStatus: state.app.selectedServerStatus,
mojangStatuses: state.app.mojangStatuses
}
}
const mapDispatch = {
@ -44,102 +43,21 @@ type InternalLandingProps = LandingProps & typeof mapDispatch
class Landing extends React.Component<InternalLandingProps, LandingState> {
private static readonly logger = LoggerUtil.getLogger('LandingTSX')
private mojangStatusInterval!: NodeJS.Timeout
private serverStatusInterval!: NodeJS.Timeout
private static readonly logger = LoggerUtil.getLogger('Landing')
constructor(props: InternalLandingProps) {
super(props)
this.state = {
mojangStatuses: [],
outdatedServerStatus: false
workingServerStatus: props.selectedServerStatus
}
}
async componentDidMount(): Promise<void> {
// Load Mojang statuses and setup refresh interval.
Landing.logger.info('Loading mojang statuses..')
await this.loadMojangStatuses()
this.mojangStatusInterval = setInterval(async () => {
Landing.logger.info('Refreshing Mojang Statuses..')
await this.loadMojangStatuses()
}, 300000)
this.serverStatusInterval = setInterval(async () => {
Landing.logger.info('Refreshing selected server status..')
this.setState({
...this.state,
outdatedServerStatus: true
})
}, 300000)
}
componentWillUnmount(): void {
// Clean up intervals.
clearInterval(this.mojangStatusInterval)
clearInterval(this.serverStatusInterval)
}
private loadMojangStatuses = async (): Promise<void> => {
const response: MojangResponse<MojangStatus[]> = await MojangRestAPI.status()
if(response.responseStatus !== RestResponseStatus.SUCCESS) {
Landing.logger.warn('Failed to retrieve Mojang Statuses.')
}
// TODO Temp workaround because their status checker always shows
// this as red. https://bugs.mojang.com/browse/WEB-2303
const statuses = response.data
for(const status of statuses) {
if(status.service === 'sessionserver.mojang.com' || status.service === 'minecraft.net') {
status.status = MojangStatusColor.GREEN
}
}
this.setState({
...this.state,
mojangStatuses: response.data
})
}
private syncServerStatus = async (): Promise<void> => {
let serverStatus: ServerStatus | undefined
if(this.props.selectedServer != null) {
const { hostname, port } = this.props.selectedServer
try {
serverStatus = await getServerStatus(
47,
hostname,
port
)
} catch(err) {
Landing.logger.error('Error while refreshing server status', err)
}
} else {
serverStatus = undefined
}
this.props.setSelectedServerStatus(serverStatus)
}
private finishServerSync = async (): Promise<void> => {
this.setState({
...this.state,
outdatedServerStatus: false
})
}
/* Mojang Status Methods */
private getMainMojangStatusColor = (): string => {
const essential = this.state.mojangStatuses.filter(s => s.essential)
const essential = this.props.mojangStatuses.filter(s => s.essential)
if(this.state.mojangStatuses.length === 0) {
if(this.props.mojangStatuses.length === 0) {
return MojangRestAPI.statusToHex(MojangStatusColor.GREY)
}
@ -152,11 +70,11 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
return MojangRestAPI.statusToHex(MojangStatusColor.YELLOW)
}
// If any non-essential are not green, return yellow.
if(this.state.mojangStatuses.filter(s => s.status !== MojangStatusColor.GREEN && s.status !== MojangStatusColor.GREY).length > 0) {
if(this.props.mojangStatuses.filter(s => s.status !== MojangStatusColor.GREEN && s.status !== MojangStatusColor.GREY).length > 0) {
return MojangRestAPI.statusToHex(MojangStatusColor.YELLOW)
}
// if all are grey, return grey.
if(this.state.mojangStatuses.filter(s => s.status === MojangStatusColor.GREY).length === this.state.mojangStatuses.length) {
if(this.props.mojangStatuses.filter(s => s.status === MojangStatusColor.GREY).length === this.props.mojangStatuses.length) {
return MojangRestAPI.statusToHex(MojangStatusColor.GREY)
}
@ -166,7 +84,7 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
private getMojangStatusesAsJSX = (essential: boolean): JSX.Element[] => {
const statuses: JSX.Element[] = []
for(const status of this.state.mojangStatuses.filter(s => s.essential === essential)) {
for(const status of this.props.mojangStatuses.filter(s => s.essential === essential)) {
statuses.push(
<div className="mojangStatusContainer" key={status.service}>
<span className="mojangStatusIcon" style={{color: MojangRestAPI.statusToHex(status.status)}}>&#8226;</span>
@ -177,18 +95,23 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
return statuses
}
/* Selected Server Methods */
private updateWorkingServerStatus = (): void => {
this.setState({
...this.state,
workingServerStatus: this.props.selectedServerStatus
})
}
private openServerSelect = (): void => {
this.props.pushServerSelectOverlay({
servers: this.props.distribution.servers,
selectedId: this.props.selectedServer.rawServer.id,
selectedId: this.props.selectedServer?.rawServer.id,
onSelection: async (serverId: string) => {
Landing.logger.info('Server Selection Change:', serverId)
const next: HeliosServer = this.props.distribution.getServerById(serverId)!
this.props.setSelectedServer(next)
this.setState({
...this.state,
outdatedServerStatus: true
})
}
})
}
@ -202,18 +125,20 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
}
private getSelectedServerStatusText = (): string => {
return this.props.selectedServerStatus != null ? 'PLAYERS' : 'SERVER'
return this.state.workingServerStatus != null ? 'PLAYERS' : 'SERVER'
}
private getSelectedServerCount = (): string => {
if(this.props.selectedServerStatus != null) {
const { online, max } = this.props.selectedServerStatus.players
if(this.state.workingServerStatus != null) {
const { online, max } = this.state.workingServerStatus.players
return `${online}/${max}`
} else {
return 'OFFLINE'
}
}
/* Render */
render(): JSX.Element {
return <>
@ -316,12 +241,11 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
<div id="content">
<CSSTransition
in={!this.state.outdatedServerStatus}
in={this.props.selectedServerStatus?.retrievedAt === this.state.workingServerStatus?.retrievedAt}
timeout={500}
classNames="serverStatusWrapper"
unmountOnExit
onEnter={this.syncServerStatus}
onExited={this.finishServerSync}
onExited={this.updateWorkingServerStatus}
>
<div id="server_status_wrapper">
<span className="bot_label" id="landingPlayerLabel">{this.getSelectedServerStatusText()}</span>

View File

@ -9,7 +9,7 @@ import '../shared-select/SharedSelect.css'
export interface ServerSelectOverlayProps {
servers: HeliosServer[]
selectedId: string
selectedId?: string
onSelection: (serverId: string) => Promise<void>
}
@ -30,7 +30,7 @@ class ServerSelectOverlay extends React.Component<InternalServerSelectOverlayPro
constructor(props: InternalServerSelectOverlayProps) {
super(props)
this.state = {
selectedId: props.selectedId
selectedId: props.selectedId!
}
}

View File

@ -31,6 +31,7 @@ ReactDOM.render(
distribution={store.getState().app.distribution!}
selectedServer={store.getState().app.selectedServer!}
selectedServerStatus={store.getState().app.selectedServerStatus!}
mojangStatuses={store.getState().app.mojangStatuses!}
/>
</Provider>
</AppContainer>,

View File

@ -1,11 +1,13 @@
import { Action } from 'redux'
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
import { ServerStatus } from 'common/mojang/net/ServerStatusAPI'
import { MojangStatus } from 'common/mojang/rest/internal/MojangStatus'
export enum AppActionType {
SetDistribution = 'SET_DISTRIBUTION',
SetSelectedServer = 'SET_SELECTED_SERVER',
SetSelectedServerStatus = 'SET_SELECTED_SERVER_STATUS'
SetSelectedServerStatus = 'SET_SELECTED_SERVER_STATUS',
SetMojangStatuses = 'SET_MOJANG_STATUSES'
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@ -23,6 +25,10 @@ export interface SetSelectedServerStatusAction extends AppAction {
payload?: ServerStatus
}
export interface SetMojangStatusesAction extends AppAction {
payload: MojangStatus[]
}
export function setDistribution(distribution?: HeliosDistribution): SetDistributionAction {
return {
type: AppActionType.SetDistribution,
@ -44,8 +50,16 @@ export function setSelectedServerStatus(serverStatus?: ServerStatus): SetSelecte
}
}
export function setMojangStatuses(mojangStatuses: MojangStatus[]): SetMojangStatusesAction {
return {
type: AppActionType.SetMojangStatuses,
payload: mojangStatuses
}
}
export const AppActionDispatch = {
setDistribution: (d?: HeliosDistribution): SetDistributionAction => setDistribution(d),
setSelectedServer: (s?: HeliosServer): SetSelectedServerAction => setSelectedServer(s),
setSelectedServerStatus: (ss?: ServerStatus): SetSelectedServerStatusAction => setSelectedServerStatus(ss)
setSelectedServerStatus: (ss?: ServerStatus): SetSelectedServerStatusAction => setSelectedServerStatus(ss),
setMojangStatuses: (ms: MojangStatus[]): SetMojangStatusesAction => setMojangStatuses(ms)
}

View File

@ -1,18 +1,22 @@
import { AppActionType, AppAction, SetDistributionAction, SetSelectedServerAction, SetSelectedServerStatusAction } from '../actions/appActions'
import { AppActionType, AppAction, SetDistributionAction, SetSelectedServerAction, SetSelectedServerStatusAction, SetMojangStatusesAction } from '../actions/appActions'
import { Reducer } from 'redux'
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
import { ServerStatus } from 'common/mojang/net/ServerStatusAPI'
import { MojangStatus } from 'common/mojang/rest/internal/MojangStatus'
import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI'
export interface AppState {
distribution?: HeliosDistribution
selectedServer?: HeliosServer
selectedServerStatus?: ServerStatus
mojangStatuses: MojangStatus[]
}
const defaultAppState: AppState = {
distribution: undefined,
selectedServer: undefined,
selectedServerStatus: undefined
selectedServerStatus: undefined,
mojangStatuses: MojangRestAPI.getDefaultStatuses()
}
const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, action) => {
@ -32,6 +36,11 @@ const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, actio
...state,
selectedServerStatus: (action as SetSelectedServerStatusAction).payload
}
case AppActionType.SetMojangStatuses:
return {
...state,
mojangStatuses: (action as SetMojangStatusesAction).payload
}
}
return state
}