Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: leverage slippi backend's getLatestDolphin query #271

Merged
merged 4 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/dolphin/checkVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ApolloClient, gql, HttpLink, InMemoryCache } from "@apollo/client";
import { isDevelopment } from "common/constants";
import { fetch } from "cross-fetch";
import { GraphQLError } from "graphql";

import { DolphinLaunchType, DolphinVersionResponse } from "./types";

const httpLink = new HttpLink({ uri: process.env.SLIPPI_GRAPHQL_ENDPOINT, fetch });

const appVersion = __VERSION__;

const client = new ApolloClient({
link: httpLink,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to provide a link here instead of just providing the uri directly? Does it not work if we don't provide it an instance to cross-fetch?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, we get this explicit error from ApolloClient

Invariant Violation:
  "fetch" has not been found globally and no fetcher has been configured. To fix this, install a fetch package (like https://www.npmjs.com/package/cross-fetch), instantiate the fetcher, and pass it into your HttpLink constructor. For example:

  import fetch from 'cross-fetch';
  import { ApolloClient, HttpLink } from '@apollo/client';
  const client = new ApolloClient({
    link: new HttpLink({ uri: '/graphql', fetch })
  });

cache: new InMemoryCache(),
name: "slippi-launcher",
version: `${appVersion}${isDevelopment ? "-dev" : ""}`,
});

const getLatestDolphinQuery = gql`
query GetLatestDolphin($purpose: DolphinPurpose, $includeBeta: Boolean) {
getLatestDolphin(purpose: $purpose, includeBeta: $includeBeta) {
linuxDownloadUrl
windowsDownloadUrl
macDownloadUrl
version
}
}
`;

const handleErrors = (errors: readonly GraphQLError[] | undefined) => {
if (errors) {
let errMsgs = "";
errors.forEach((err) => {
errMsgs += `${err.message}\n`;
});
throw new Error(errMsgs);
}
};

export async function fetchLatestDolphin(
dolphinType: DolphinLaunchType,
beta = false,
): Promise<DolphinVersionResponse> {
const res = await client.query({
query: getLatestDolphinQuery,
fetchPolicy: "network-only",
variables: {
purpose: dolphinType.toUpperCase(),
includeBeta: beta,
},
});

handleErrors(res.errors);

return {
version: res.data.getLatestDolphin.version,
downloadUrls: {
darwin: res.data.getLatestDolphin.macDownloadUrl,
linux: res.data.getLatestDolphin.linuxDownloadUrl,
win32: res.data.getLatestDolphin.windowsDownloadUrl,
},
};
}
110 changes: 39 additions & 71 deletions src/dolphin/downloadDolphin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AdmZip from "adm-zip";
import { ChildProcessWithoutNullStreams, spawn, spawnSync } from "child_process";
import { spawnSync } from "child_process";
import { isLinux } from "common/constants";
import { app, BrowserWindow } from "electron";
import { download } from "electron-dl";
Expand All @@ -10,15 +10,26 @@ import path from "path";
import { lt } from "semver";

import { fileExists } from "../main/fileExists";
import { getLatestRelease } from "../main/github";
import { fetchLatestDolphin } from "./checkVersion";
import { ipc_dolphinDownloadFinishedEvent, ipc_dolphinDownloadLogReceivedEvent } from "./ipc";
import { DolphinLaunchType } from "./types";
import { DolphinLaunchType, DolphinVersionResponse } from "./types";
import { findDolphinExecutable } from "./util";

function logDownloadInfo(message: string): void {
void ipc_dolphinDownloadLogReceivedEvent.main!.trigger({ message });
}

export async function assertDolphinInstallations(): Promise<void> {
try {
await assertDolphinInstallation(DolphinLaunchType.NETPLAY, logDownloadInfo);
await assertDolphinInstallation(DolphinLaunchType.PLAYBACK, logDownloadInfo);
Comment on lines +24 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we could use Promise.all so we could potentially avoid a waterfall. However, since we're sending the log to logDownloadInfo we might not be able to do that, since it would intersperse the log messages. Something we could keep in mind for the future though.

await ipc_dolphinDownloadFinishedEvent.main!.trigger({ error: null });
} catch (err) {
console.error(err);
await ipc_dolphinDownloadFinishedEvent.main!.trigger({ error: err.message });
}
}

export async function assertDolphinInstallation(
type: DolphinLaunchType,
log: (message: string) => void,
Expand All @@ -27,106 +38,63 @@ export async function assertDolphinInstallation(
await findDolphinExecutable(type);
log(`Found existing ${type} Dolphin executable.`);
log(`Checking if we need to update ${type} Dolphin`);
const data = await getLatestReleaseData(type);
const latestVersion = data.tag_name;
const dolphinDownloadInfo = await fetchLatestDolphin(type);
const latestVersion = dolphinDownloadInfo.version;
const isOutdated = await compareDolphinVersion(type, latestVersion);
if (isOutdated) {
log(`${type} Dolphin installation is outdated. Downloading latest...`);
await downloadAndInstallDolphin(type, log);
await downloadAndInstallDolphin(type, dolphinDownloadInfo, log);
return;
}
log("No update found...");
return;
} catch (err) {
log(`Could not find ${type} Dolphin installation. Downloading...`);
await downloadAndInstallDolphin(type, log);
const dolphinDownloadInfo = await fetchLatestDolphin(type);
await downloadAndInstallDolphin(type, dolphinDownloadInfo, log);
}
}

export async function downloadAndInstallDolphin(
type: DolphinLaunchType,
log: (message: string) => void,
cleanInstall = false,
): Promise<void> {
const downloadedAsset = await downloadLatestDolphin(type, log);
log(`Installing ${type} Dolphin...`);
await installDolphin(type, downloadedAsset, log, cleanInstall);
log(`Finished ${type} installing`);
}

export async function assertDolphinInstallations(): Promise<void> {
try {
await assertDolphinInstallation(DolphinLaunchType.NETPLAY, logDownloadInfo);
await assertDolphinInstallation(DolphinLaunchType.PLAYBACK, logDownloadInfo);
await ipc_dolphinDownloadFinishedEvent.main!.trigger({ error: null });
} catch (err) {
console.error(err);
await ipc_dolphinDownloadFinishedEvent.main!.trigger({ error: err.message });
}
return;
}

async function compareDolphinVersion(type: DolphinLaunchType, latestVersion: string): Promise<boolean> {
const dolphinPath = await findDolphinExecutable(type);
const dolphinVersion = spawnSync(dolphinPath, ["--version"]).stdout.toString();
return lt(dolphinVersion, latestVersion);
}

export async function openDolphin(type: DolphinLaunchType, params?: string[]): Promise<ChildProcessWithoutNullStreams> {
const dolphinPath = await findDolphinExecutable(type);
return spawn(dolphinPath, params);
}

async function getLatestDolphinAsset(type: DolphinLaunchType): Promise<any> {
const release = await getLatestReleaseData(type);
const asset = release.assets.find((a: any) => matchesPlatform(a.name));
if (!asset) {
throw new Error(`No release asset matched the current platform: ${process.platform}`);
}
return asset;
}

async function getLatestReleaseData(type: DolphinLaunchType): Promise<any> {
const owner = "project-slippi";
let repo = "Ishiiruka";
if (type === DolphinLaunchType.PLAYBACK) {
repo += "-Playback";
}
return getLatestRelease(owner, repo);
}

function matchesPlatform(releaseName: string): boolean {
switch (process.platform) {
case "win32":
return releaseName.endsWith("Win.zip");
case "darwin":
return releaseName.endsWith("Mac.dmg");
case "linux":
return releaseName.endsWith("Linux.zip");
default:
return false;
}
export async function downloadAndInstallDolphin(
type: DolphinLaunchType,
releaseInfo: DolphinVersionResponse,
log: (message: string) => void,
cleanInstall = false,
): Promise<void> {
const downloadUrl = releaseInfo.downloadUrls[process.platform];
const downloadedAsset = await downloadLatestDolphin(downloadUrl, log);
log(`Installing v${releaseInfo.version} ${type} Dolphin...`);
await installDolphin(type, downloadedAsset, log, cleanInstall);
log(`Finished v${releaseInfo.version} ${type} Dolphin install`);
}

async function downloadLatestDolphin(
type: DolphinLaunchType,
downloadUrl: string,
log: (status: string) => void = console.log,
): Promise<string> {
const asset = await getLatestDolphinAsset(type);
const parsedUrl = new URL(downloadUrl);
const filename = path.basename(parsedUrl.pathname);

const downloadDir = path.join(app.getPath("userData"), "temp");
await fs.ensureDir(downloadDir);
const downloadLocation = path.join(downloadDir, asset.name);
const downloadLocation = path.join(downloadDir, filename);
const exists = await fileExists(downloadLocation);
if (!exists) {
log(`Downloading ${asset.browser_download_url} to ${downloadLocation}`);
log(`Downloading ${downloadUrl} to ${downloadLocation}`);
const win = BrowserWindow.getFocusedWindow();
if (win) {
await download(win, asset.browser_download_url, {
filename: asset.name,
await download(win, downloadUrl, {
filename: filename,
Comment on lines +92 to +93
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a comment, but I think it would be good if we could eventually not depend on the existence of a browser window to download a file since electron-dl seems to require a Browser window. I feel like there are a few cases where the app is running without a window (e.g. MacOS) or in the task bar/system tray (perhaps in the future).

directory: downloadDir,
onProgress: (progress) => log(`Downloading... ${(progress.percent * 100).toFixed(0)}%`),
});
log(`Successfully downloaded ${asset.browser_download_url} to ${downloadLocation}`);
log(`Successfully downloaded ${downloadUrl} to ${downloadLocation}`);
} else {
log("I dunno how we got here, but apparently there isn't a browser window /shrug");
}
Expand Down
4 changes: 3 additions & 1 deletion src/dolphin/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as fs from "fs-extra";
import { fileExists } from "main/fileExists";
import path from "path";

import { fetchLatestDolphin } from "./checkVersion";
import { downloadAndInstallDolphin } from "./downloadDolphin";
import { DolphinInstance, PlaybackDolphinInstance } from "./instance";
import { DolphinLaunchType, ReplayCommunication } from "./types";
Expand Down Expand Up @@ -126,7 +127,8 @@ export class DolphinManager extends EventEmitter {
}

// No dolphins of launchType are open so lets reinstall
await downloadAndInstallDolphin(launchType, log.info, true);
const releaseInfo = await fetchLatestDolphin(launchType);
await downloadAndInstallDolphin(launchType, releaseInfo, log.info, true);
const isoPath = settingsManager.get().settings.isoPath;
if (isoPath) {
const gameDir = path.dirname(isoPath);
Expand Down
9 changes: 9 additions & 0 deletions src/dolphin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,12 @@ export interface PlayKey {
displayName: string;
latestVersion: string;
}

export interface DolphinVersionResponse {
version: string;
downloadUrls: {
darwin: string;
linux: string;
win32: string;
};
}
3 changes: 2 additions & 1 deletion src/renderer/lib/slippiBackend.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApolloClient, ApolloLink, gql, HttpLink, InMemoryCache } from "@apollo/client";
import { ipc_checkPlayKeyExists, ipc_removePlayKeyFile, ipc_storePlayKeyFile } from "@dolphin/ipc";
import { PlayKey } from "@dolphin/types";
import { isDevelopment } from "common/constants";
import electronLog from "electron-log";
import firebase from "firebase";
import { GraphQLError } from "graphql";
Expand All @@ -15,7 +16,7 @@ const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
name: "slippi-launcher",
version: appVersion,
version: `${appVersion}${isDevelopment ? "-dev" : ""}`,
});

const getUserKeyQuery = gql`
Expand Down
8 changes: 8 additions & 0 deletions webpack.main.additions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const ThreadsPlugin = require("threads-plugin");
const Dotenv = require("dotenv-webpack");
const { DefinePlugin } = require("webpack");
const pkg = require("./package.json");

module.exports = function (context) {
// Enforce chunkhash when building output files.
Expand Down Expand Up @@ -29,5 +31,11 @@ module.exports = function (context) {
new Dotenv(),
);

context.plugins.push(
new DefinePlugin({
__VERSION__: JSON.stringify(pkg.version),
}),
);

return context;
};