Compare commits

...

1 Commits

Author SHA1 Message Date
Clément Pasteau
b21a191d4c wip 2021-12-21 17:20:20 +01:00
2 changed files with 303 additions and 243 deletions

View File

@@ -0,0 +1,88 @@
// @flow
import { type Build, getBuild } from '../../Utils/GDevelopServices/Build';
import { delay } from '../../Utils/Delay';
import { type AuthenticatedUser } from '../../Profile/AuthenticatedUserContext';
const waitTime = 1500;
const bulkWaitTime = 5000;
const maxTimeBeforeIgnoring = 12 * 60 * 60 * 1000; // 12 hours in milliseconds
export const startWatchingBuilds = (
authenticatedUser: AuthenticatedUser,
builds: Array<Build>,
onBuildUpdated: (build: Build) => void,
runningWatchers: { [string]: boolean } = {},
setRunningWatchers: ({ [string]: boolean }) => void
) => {
console.log('starting watching build');
stopWatchingBuilds(setRunningWatchers);
builds.forEach(build => {
if (build.status === 'pending') {
if (
(!build.createdAt ||
build.createdAt < Date.now() - maxTimeBeforeIgnoring) &&
(!build.updatedAt ||
build.updatedAt < Date.now() - maxTimeBeforeIgnoring)
) {
console.info(
"Ignoring a build for polling as it's too old and still pending",
build
);
} else {
pollBuild(
build.id,
builds.length > 1 ? bulkWaitTime : waitTime,
runningWatchers,
setRunningWatchers,
authenticatedUser,
onBuildUpdated
);
}
}
});
};
export const stopWatchingBuilds = (
setRunningWatchers: ({ [string]: boolean }) => void
) => {
setRunningWatchers({});
};
const pollBuild = async (
buildId: string,
waitTime: number,
runningWatchers: { [string]: boolean },
setRunningWatchers: ({ [string]: boolean }) => void,
authenticatedUser: ?AuthenticatedUser,
onBuildUpdated: ?(build: Build) => void
) => {
console.log('polling build');
setRunningWatchers({
...runningWatchers,
[buildId]: true,
});
let build = null;
do {
if (!authenticatedUser) return;
const { getAuthorizationHeader, firebaseUser } = authenticatedUser;
if (!firebaseUser) return;
try {
console.info(`Checking progress of build ${buildId}...`);
build = await getBuild(getAuthorizationHeader, firebaseUser.uid, buildId);
if (onBuildUpdated) onBuildUpdated(build);
} catch (e) {
console.warn('Error while watching build progress:', e);
}
await delay(waitTime);
if (!runningWatchers[buildId]) {
console.info(`Stopping watch of build ${buildId}.`);
return;
}
} while (build && build.status === 'pending');
console.info(`Watch of build ${buildId} finished.`);
};

View File

@@ -1,6 +1,6 @@
// @flow
import React, { Component } from 'react';
import React from 'react';
import { I18n } from '@lingui/react';
import { t, Trans } from '@lingui/macro';
import RaisedButton from '../../UI/RaisedButton';
@@ -20,8 +20,10 @@ import {
displayProjectErrorsBox,
getProjectPropertiesErrors,
} from '../../Utils/ProjectErrorsChecker';
import { type Limit } from '../../Utils/GDevelopServices/Usage';
import BuildsWatcher from '../Builds/BuildsWatcher';
import {
startWatchingBuilds,
stopWatchingBuilds,
} from '../Builds/BuildsWatcherNew';
import BuildStepsProgress, {
type BuildStep,
} from '../Builds/BuildStepsProgress';
@@ -39,17 +41,6 @@ import {
TRIVIAL_FIRST_WEB_EXPORT,
} from '../../Utils/GDevelopServices/Badge';
type State = {|
exportStep: BuildStep,
compressionOutput: any,
build: ?Build,
stepCurrentProgress: number,
stepMaxProgress: number,
errored: boolean,
exportState: any,
doneFooterOpen: boolean,
|};
type Props = {|
project: gdProject,
onChangeSubscription: () => void,
@@ -61,78 +52,100 @@ type Props = {|
* A generic UI to launch, monitor the progres and get the result
* of an export.
*/
export default class ExportLauncher extends Component<Props, State> {
state = {
exportStep: '',
build: null,
compressionOutput: null,
stepCurrentProgress: 0,
stepMaxProgress: 0,
doneFooterOpen: false,
errored: false,
exportState: this.props.exportPipeline.getInitialExportState(
this.props.project
),
};
buildsWatcher = new BuildsWatcher();
launchWholeExport: () => Promise<void>;
export default function ExportLauncher({
project,
onChangeSubscription,
authenticatedUser,
exportPipeline,
}: Props) {
const [exportStep, setExportStep] = React.useState<BuildStep>('');
const [build, setBuild] = React.useState<?Build>(null);
const [compressionOutput, setCompressionOutput] = React.useState(null);
const [stepCurrentProgress, setStepCurrentProgress] = React.useState(0);
const [stepMaxProgress, setStepMaxProgress] = React.useState(0);
const [errored, setErrored] = React.useState(false);
const [exportState, setExportState] = React.useState(
exportPipeline.getInitialExportState(project)
);
const [doneFooterOpen, setDoneFooterOpen] = React.useState(false);
const [buildWatchers, setBuildWatchers] = React.useState({});
componentWillUnmount() {
this.buildsWatcher.stop();
}
// Cleanup
React.useEffect(() => {
return () => stopWatchingBuilds(setBuildWatchers);
});
constructor(props: Props) {
super(props);
this._setupAchievementHook();
}
componentDidUpdate(prevProps: Props, prevState: State) {
this._setupAchievementHook();
}
const buildLimit = React.useMemo(
() =>
authenticatedUser.limits && exportPipeline.onlineBuildType
? authenticatedUser.limits[exportPipeline.onlineBuildType]
: null,
[authenticatedUser, exportPipeline.onlineBuildType]
);
_setupAchievementHook = () => {
if (
ACHIEVEMENT_FEATURE_FLAG &&
this.props.exportPipeline.name.includes('web')
) {
this.launchWholeExport = addCreateBadgePreHookIfNotClaimed(
this.props.authenticatedUser,
TRIVIAL_FIRST_WEB_EXPORT,
this._launchWholeExport
);
} else {
this.launchWholeExport = this._launchWholeExport;
}
};
const canLaunchBuild = React.useMemo(
() => {
const buildPending =
!errored && exportStep !== '' && exportStep !== 'done';
const buildFinished = !errored && exportStep === 'done';
if (buildPending || buildFinished) return false;
_updateStepProgress = (
if (buildLimit && buildLimit.limitReached) return false;
return exportPipeline.canLaunchBuild(exportState);
},
[errored, exportStep, exportState, buildLimit, exportPipeline]
);
const updateStepProgress = (
stepCurrentProgress: number,
stepMaxProgress: number
) =>
this.setState({
stepCurrentProgress,
stepMaxProgress,
});
_startBuildWatch = (authenticatedUser: AuthenticatedUser) => {
if (!this.state.build) return;
this.buildsWatcher.start({
authenticatedUser,
builds: [this.state.build],
onBuildUpdated: (build: Build) => {
this.setState({ build });
authenticatedUser.onRefreshUserProfile();
},
});
) => {
setStepCurrentProgress(stepCurrentProgress);
setStepMaxProgress(stepMaxProgress);
};
registerAndUpdateGame = async () => {
const profile = this.props.authenticatedUser.profile;
const getAuthorizationHeader = this.props.authenticatedUser
.getAuthorizationHeader;
const gameId = this.props.project.getProjectUuid();
const authorName = this.props.project.getAuthor() || 'Unspecified author';
const gameName = this.props.project.getName() || 'Untitled game';
const startBuildWatch = React.useCallback(
() => {
console.log('starting build watch', build);
// Build is finished or already being watched.
if (!build || build.status !== 'pending' || buildWatchers[build.id])
return;
console.log(build, buildWatchers);
console.log('properly starting build watch', build);
startWatchingBuilds(
authenticatedUser,
[build],
build => {
setBuild(build);
if (build.status !== 'pending') {
// Refresh user limits.
authenticatedUser.onRefreshUserProfile();
}
},
buildWatchers,
setBuildWatchers
);
},
[authenticatedUser, build, buildWatchers]
);
React.useEffect(
() => {
if (!build) return;
startBuildWatch();
},
[build, startBuildWatch]
);
const registerAndUpdateGame = async () => {
const profile = authenticatedUser.profile;
const getAuthorizationHeader = authenticatedUser.getAuthorizationHeader;
const gameId = project.getProjectUuid();
const authorName = project.getAuthor() || 'Unspecified author';
const gameName = project.getName() || 'Untitled game';
if (profile) {
const userId = profile.id;
try {
@@ -156,16 +169,15 @@ export default class ExportLauncher extends Component<Props, State> {
}
};
_launchWholeExport = async () => {
const _launchWholeExport = async () => {
const t = str => str; //TODO;
const { project, exportPipeline, authenticatedUser } = this.props;
sendExportLaunched(exportPipeline.name);
if (!displayProjectErrorsBox(t, getProjectPropertiesErrors(t, project)))
return;
const getErrorMessage = () => {
switch (this.state.exportStep) {
switch (exportStep) {
case 'export':
return t('Error while preparing the exporter.');
case 'resources-download':
@@ -190,16 +202,14 @@ export default class ExportLauncher extends Component<Props, State> {
};
const handleError = (err: Error) => {
if (!this.state.errored) {
this.setState({
errored: true,
});
if (!errored) {
setErrored(true);
showErrorBox({
message:
getErrorMessage() +
(err.message ? `\n\nDetails of the error: ${err.message}` : ''),
rawError: {
exportStep: this.state.exportStep,
exportStep: exportStep,
rawError: err,
},
errorId: 'export-error',
@@ -207,11 +217,9 @@ export default class ExportLauncher extends Component<Props, State> {
}
};
const setStep = (step: BuildStep) => this.setState({ exportStep: step });
try {
// We do not await for this call, allowing to start building the game in parallel.
this.registerAndUpdateGame();
registerAndUpdateGame();
} catch {
// Best effort call, we don't prevent building the game.
console.warn('Error while registering the game - ignoring it.');
@@ -220,16 +228,14 @@ export default class ExportLauncher extends Component<Props, State> {
try {
const exportPipelineContext = {
project,
updateStepProgress: this._updateStepProgress,
exportState: this.state.exportState,
updateStepProgress,
exportState: exportState,
};
setStep('export');
this.setState({
stepCurrentProgress: 0,
stepMaxProgress: 0,
errored: false,
build: null,
});
setExportStep('export');
setStepCurrentProgress(0);
setStepMaxProgress(0);
setErrored(false);
setBuild(null);
const preparedExporter = await exportPipeline.prepareExporter(
exportPipelineContext
);
@@ -237,184 +243,150 @@ export default class ExportLauncher extends Component<Props, State> {
exportPipelineContext,
preparedExporter
);
setStep('resources-download');
setExportStep('resources-download');
const resourcesDownloadOutput = await exportPipeline.launchResourcesDownload(
exportPipelineContext,
exportOutput
);
setStep('compress');
setExportStep('compress');
const compressionOutput = await exportPipeline.launchCompression(
exportPipelineContext,
resourcesDownloadOutput
);
const { launchUpload, launchOnlineBuild } = exportPipeline;
if (!!launchUpload && !!launchOnlineBuild) {
setStep('upload');
setExportStep('upload');
const uploadBucketKey = await launchUpload(
exportPipelineContext,
compressionOutput
);
setStep('waiting-for-build');
setExportStep('waiting-for-build');
const build = await launchOnlineBuild(
this.state.exportState,
exportState,
authenticatedUser,
uploadBucketKey
);
setStep('build');
this.setState({ build }, () => {
this._startBuildWatch(authenticatedUser);
});
setExportStep('build');
setBuild(build);
}
setStep('done');
this.setState({
compressionOutput,
doneFooterOpen: true,
});
setExportStep('done');
setDoneFooterOpen(true);
setCompressionOutput(compressionOutput);
} catch (error) {
console.error('An error happened during export:', error);
handleError(error);
}
};
_downloadBuild = (key: BuildArtifactKeyName) => {
const url = getBuildArtifactUrl(this.state.build, key);
const launchWholeExport =
ACHIEVEMENT_FEATURE_FLAG && exportPipeline.name.includes('web')
? addCreateBadgePreHookIfNotClaimed(
authenticatedUser,
TRIVIAL_FIRST_WEB_EXPORT,
_launchWholeExport
)
: _launchWholeExport;
const downloadBuild = (key: BuildArtifactKeyName) => {
const url = getBuildArtifactUrl(build, key);
if (url) Window.openExternalURL(url);
};
_closeDoneFooter = () =>
this.setState({
doneFooterOpen: false,
});
_updateExportState = (updater: any => any) => {
this.setState(prevState => ({
...prevState,
exportState: updater(prevState.exportState),
}));
const updateExportState = (updater: any => any) => {
setExportState(updater(exportState));
};
render() {
const {
exportStep,
compressionOutput,
build,
stepMaxProgress,
stepCurrentProgress,
errored,
doneFooterOpen,
exportState,
} = this.state;
const { project, authenticatedUser, exportPipeline } = this.props;
if (!project) return null;
if (!project) return null;
const getBuildLimit = (authenticatedUser: AuthenticatedUser): ?Limit =>
authenticatedUser.limits && exportPipeline.onlineBuildType
? authenticatedUser.limits[exportPipeline.onlineBuildType]
: null;
const canLaunchBuild = (authenticatedUser: AuthenticatedUser) => {
const buildPending =
!errored && exportStep !== '' && exportStep !== 'done';
const buildFinished = !errored && exportStep === 'done';
if (buildPending || buildFinished) return false;
const limit: ?Limit = getBuildLimit(authenticatedUser);
if (limit && limit.limitReached) return false;
return exportPipeline.canLaunchBuild(exportState);
};
return (
<Column noMargin>
{!!exportPipeline.packageNameWarningType &&
project.getPackageName().indexOf('com.example') !== -1 && (
<Line>
<DismissableAlertMessage
identifier="project-should-have-unique-package-name"
kind="warning"
>
<I18n>
{({ i18n }) =>
i18n._(
exportPipeline.packageNameWarningType === 'mobile'
? t`The package name begins with com.example, make sure you replace it with an unique one to be able to publish your game on app stores.`
: t`The package name begins with com.example, make sure you replace it with an unique one, else installing your game might overwrite other games.`
)
}
</I18n>
</DismissableAlertMessage>
</Line>
)}
<Line>
{exportPipeline.renderHeader({
project,
exportState,
updateExportState: this._updateExportState,
})}
</Line>
{(!exportPipeline.onlineBuildType ||
authenticatedUser.authenticated) && (
<Line justifyContent="center">
<RaisedButton
label={exportPipeline.renderLaunchButtonLabel()}
primary
onClick={this.launchWholeExport}
disabled={!canLaunchBuild(authenticatedUser)}
/>
return (
<Column noMargin>
{!!exportPipeline.packageNameWarningType &&
project.getPackageName().indexOf('com.example') !== -1 && (
<Line>
<DismissableAlertMessage
identifier="project-should-have-unique-package-name"
kind="warning"
>
<I18n>
{({ i18n }) =>
i18n._(
exportPipeline.packageNameWarningType === 'mobile'
? t`The package name begins with com.example, make sure you replace it with an unique one to be able to publish your game on app stores.`
: t`The package name begins with com.example, make sure you replace it with an unique one, else installing your game might overwrite other games.`
)
}
</I18n>
</DismissableAlertMessage>
</Line>
)}
<Spacer />
{!!exportPipeline.onlineBuildType &&
!authenticatedUser.authenticated && (
<CreateProfile
onLogin={authenticatedUser.onLogin}
onCreateAccount={authenticatedUser.onCreateAccount}
message={
<Trans>
Create an account or login first to publish your game.
</Trans>
}
justifyContent="center"
/>
)}
{authenticatedUser.authenticated &&
(exportPipeline.renderCustomStepsProgress ? (
exportPipeline.renderCustomStepsProgress(
build,
!!this.state.exportStep && this.state.exportStep !== 'done'
)
) : (
<Line expand>
<BuildStepsProgress
exportStep={exportStep}
hasBuildStep={!!exportPipeline.onlineBuildType}
build={build}
onDownload={this._downloadBuild}
stepMaxProgress={stepMaxProgress}
stepCurrentProgress={stepCurrentProgress}
errored={errored}
/>
</Line>
))}
{!!exportPipeline.limitedBuilds && authenticatedUser.authenticated && (
<LimitDisplayer
subscription={authenticatedUser.subscription}
limit={getBuildLimit(authenticatedUser)}
onChangeSubscription={this.props.onChangeSubscription}
<Line>
{exportPipeline.renderHeader({
project,
exportState,
updateExportState,
})}
</Line>
{(!exportPipeline.onlineBuildType || authenticatedUser.authenticated) && (
<Line justifyContent="center">
<RaisedButton
label={exportPipeline.renderLaunchButtonLabel()}
primary
onClick={launchWholeExport}
disabled={!canLaunchBuild}
/>
)}
{doneFooterOpen &&
exportPipeline.renderDoneFooter &&
exportPipeline.renderDoneFooter({
compressionOutput,
exportState,
onClose: this._closeDoneFooter,
})}
{doneFooterOpen && (
<Line justifyContent="center">
<GameRegistration project={project} hideIfSubscribed hideLoader />
</Line>
)}
<Spacer />
{!!exportPipeline.onlineBuildType && !authenticatedUser.authenticated && (
<CreateProfile
onLogin={authenticatedUser.onLogin}
onCreateAccount={authenticatedUser.onCreateAccount}
message={
<Trans>
Create an account or login first to publish your game.
</Trans>
}
justifyContent="center"
/>
)}
{authenticatedUser.authenticated &&
(exportPipeline.renderCustomStepsProgress ? (
exportPipeline.renderCustomStepsProgress(
build,
!!exportStep && exportStep !== 'done'
)
) : (
<Line expand>
<BuildStepsProgress
exportStep={exportStep}
hasBuildStep={!!exportPipeline.onlineBuildType}
build={build}
onDownload={downloadBuild}
stepMaxProgress={stepMaxProgress}
stepCurrentProgress={stepCurrentProgress}
errored={errored}
/>
</Line>
)}
</Column>
);
}
))}
{!!exportPipeline.limitedBuilds && authenticatedUser.authenticated && (
<LimitDisplayer
subscription={authenticatedUser.subscription}
limit={buildLimit}
onChangeSubscription={onChangeSubscription}
/>
)}
{doneFooterOpen &&
exportPipeline.renderDoneFooter &&
exportPipeline.renderDoneFooter({
compressionOutput,
exportState,
onClose: () => setDoneFooterOpen(false),
})}
{doneFooterOpen && (
<Line justifyContent="center">
<GameRegistration project={project} hideIfSubscribed hideLoader />
</Line>
)}
</Column>
);
}