-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix: missing lowercase comparison in electron-updater #8992
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
Conversation
🦋 Changeset detectedLatest commit: 2c9de85 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This ensures that AppImage is detected correctly
Currently it uses the URL which creates invalid paths like /pending/temp-http:/myurl.com/latest
@mmaietta Is this error on mac expected? I didnt really touch that code |
Retriggered the unit tests and the flaky ones ( Can you please add a changeset to this PR using |
Done @mmaietta |
Further revert
I also encountered this problem because the download URL returned by our server is https://xxx.xxx.com/xxx/d0le6oc2v2fgt7i7ejag This format results in the getCacheUpdatingFileName method going to return taskOptions. fileInfo. info. url. |
I agree @smileToWxm, for my part I decided to fork the electron-updater and do my own fixes. I removed a bunch of hacks and simplified the logic a bunch. I think the electron-updater is due for a v2, there are simpler ways now to do auto-update. We should take a page out of vscode/signal auto-update logic which is very simple and works well across OS. |
@Sytten I'm currently converting electron-builder and electron-updater to ESM-only in next major semver bump in order to support latest updates to Can you link me to their auto-update logic? I can investigate further on how we could simplify the implementation. Just to kickstart the conversation, could you elaborate on what you mean about this?
|
For windows (other files are nearby) Couple things we might want to try:
|
Oh my indeed, there's a snap updater too! Might be able to bring that over once I peruse the code further
Wouldn't this invalidate the signature of the application? Or make it so additional installer/updater nsh scripts don't run? I think electron-builder may not be able to do this since we can't guarantee the format/setup the end-user/developer has currated for their application. I'll look into the signature verification. I know we already do check that for Windows as well as for Mac. Linux doesn't have any signature validation though. Multiple channels should already be supported IIRC. Can you elaborate on this one? I can write a tutorial for it. Curious as to what you find isn't trivial though (like where to start? Or how to configure the server to accept the format? Not sure where I should focus for a tutorial) |
@mmaietta Right make sense for asar. I guess it depends on how the app is built. Multiple channels I don't super well remember. I think for the custom providers, my main issue was injecting the file in the deb to differentiate between AppImage and deb. Here is my code: import type {
AppUpdater,
Logger,
ProviderRuntimeOptions,
ResolvedUpdateFileInfo,
UpdateInfo,
} from "@caido/electron-updater";
import { Provider } from "@caido/electron-updater";
import { z } from "zod";
import type { Release } from "./schema";
import { releaseSchema } from "./schema";
const BASE_URL = "https://caido.download/releases/";
type CaidoProviderOptions = {
provider: "custom";
channel?: string;
};
export class CaidoProvider extends Provider<UpdateInfo> {
url: URL;
channel: string;
// eslint-disable-next-line
log: Logger | null;
constructor(
options: CaidoProviderOptions,
updater: AppUpdater,
runtimeOptions: ProviderRuntimeOptions,
) {
super(runtimeOptions);
this.channel = options.channel ?? "latest";
this.url = new URL(BASE_URL + this.channel);
this.log = updater.logger;
}
async getLatestVersion(): Promise<UpdateInfo> {
// Fetch the latest release
const raw = await this.httpRequest(this.url);
if (raw === null) {
throw new Error("Failed to fetch latest release");
}
let release: Release;
try {
release = parseRelease(raw);
} catch (error) {
this.log?.error(`Failed to parse release: ${error}`);
throw new Error("Failed to parse release");
}
// Find the links for the current platform
// NOTE: The AppUpdater will take care of selecting the correct link
const platform = computePlatform();
const links = release.links
.filter((link) => link.kind === "desktop" && link.platform === platform)
.map((link) => ({
url: link.link,
sha512: link.hash,
}));
if (links.length === 0) {
throw new Error(`No desktop link found for platform: ${platform}`);
}
return {
version: release.version,
files: links,
releaseDate: release.released_at,
} as UpdateInfo;
}
resolveFiles(updateInfo: UpdateInfo): Array<ResolvedUpdateFileInfo> {
return updateInfo.files.map((file) => ({
url: new URL(file.url),
info: file,
}));
}
}
function computePlatform(): string {
let os = "";
switch (process.platform) {
case "darwin":
os = "mac";
break;
case "win32":
os = "win";
break;
case "linux":
os = "linux";
break;
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
let arch = "";
switch (process.arch) {
case "arm64":
arch = "aarch64";
break;
case "x64":
arch = "x86_64";
break;
default:
throw new Error(`Unsupported architecture: ${process.arch}`);
}
return `${os}-${arch}`;
}
function parseRelease(raw: string): Release {
return z
.string()
.transform((str, ctx) => {
try {
return JSON.parse(str);
} catch (error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid JSON",
});
return z.never;
}
})
.pipe(releaseSchema)
.parse(raw);
} import { z } from "zod";
export const releaseLinkSchema = z.object({
display: z.string(),
platform: z.string(),
kind: z.string(),
link: z.string(),
hash: z.string(),
});
export type ReleaseLink = z.infer<typeof releaseLinkSchema>;
export const releaseSchema = z.object({
id: z.string(),
version: z.string(),
links: z.array(releaseLinkSchema),
released_at: z.string(),
});
export type Release = z.infer<typeof releaseSchema>; import { readFileSync } from "fs";
import path from "path";
import process from "process";
import {
AppImageUpdater,
DebUpdater,
MacUpdater,
NsisUpdater,
} from "@caido/electron-updater";
import type { AppUpdater, Logger } from "@caido/electron-updater";
import { CaidoProvider } from "./provider";
export function createUpdater(log: Logger, channel?: string): AppUpdater {
const provider = {
provider: "custom",
channel,
updateProvider: CaidoProvider,
} as const;
let updater: AppUpdater;
if (process.platform === "win32") {
log.info("On Windows, assuming Nsis installer");
updater = new NsisUpdater(provider);
} else if (process.platform === "darwin") {
log.info("On macOS, assuming Mac updater");
updater = new MacUpdater(provider);
} else {
log.info("On Linux, assuming AppImage updater");
updater = new AppImageUpdater(provider);
// Check if it's a another package
try {
const identity = path.join(process.resourcesPath, "package-type");
const fileType = readFileSync(identity).toString().trim();
switch (fileType) {
case "deb":
log.info("Detected deb package");
updater = new DebUpdater(provider);
break;
default:
break;
}
} catch (error) {
log.info("Unable to detect 'package-type' for autoUpdater");
}
}
updater.disableDifferentialDownload = true;
updater.autoDownload = false;
updater.logger = log;
updater.forceDevUpdateConfig = true;
return updater;
} Then to generate the import { Publisher } from "electron-publish";
// Bogus publisher to generate the package-type
export default class CaidoPublisher extends Publisher {
constructor(context) {
super(context);
}
get providerName() {
return "Caido";
}
upload(task) {
return Promise.resolve();
}
} I also had to create a bogus provider: custom
updaterCacheDirName: caido-updater Then in the config extraResources: [
{
from: "resources/bin",
to: "bin",
},
{
from: "resources/app-update.yml",
to: "app-update.yml",
},
],
linux: {
executableName: "caido",
icon: "icons/icon_256x256.png",
target: ["AppImage", "deb", "tar.gz"],
category: "Network",
publish: { provider: "custom" }, // Required to generate the package-type
} Lets just say it was kinda of a PITA to make it all work. And I hit the bug of the lowercase / bad file rename that this PR fixed |
I also made modifications after forking, but I don’t know how to reference the modified library in package.json, because the forked one is the entire electron-builder, how can I only reference the electron-updater in it? |
You might be able to just add the dependency to your package.json if you utilize your fork as a git submodule, then use |
Ok, ok, I'll give it a try on my end. Thank you |
Each commit explains why the fix was required, but TLDR:
https://myurl.com/myfile.AppImage
can never be equal toAppImage
. I think it worked for most people because of the fallback 3 that takes whatever first file which is not in thenot
. In my case this file was atar.gz
so that didn't work.else
clause to generate the download file name which is based on the url. This generates invalid paths on the disk since it tries to create a filepending/temp-https:/myurl.com/myfile.tar.gz
. I decided to use a sha1 instead of random so at least it will be stable between restarts since we check if the file is already downloaded.We might want to change the logic of
getCacheUpdateFileName
to something simpler and uniform IMO, but I don't fully know the implications of that change.