Skip to content

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

Merged
merged 4 commits into from
Apr 11, 2025

Conversation

Sytten
Copy link
Contributor

@Sytten Sytten commented Mar 27, 2025

Each commit explains why the fix was required, but TLDR:

  • Without a lowercase on the file extension, https://myurl.com/myfile.AppImage can never be equal to AppImage. I think it worked for most people because of the fallback 3 that takes whatever first file which is not in the not. In my case this file was a tar.gz so that didn't work.
  • Since the extension didn't match it also took the 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 file pending/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.

Copy link

changeset-bot bot commented Mar 27, 2025

🦋 Changeset detected

Latest commit: 2c9de85

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
electron-updater Patch

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

Sytten added 2 commits March 27, 2025 17:44
This ensures that AppImage is detected correctly
Currently it uses the URL which creates invalid paths like /pending/temp-http:/myurl.com/latest
@Sytten Sytten changed the title Fix multiple electron-updater bugs fix: fix multiple electron-updater bugs Mar 27, 2025
@Sytten Sytten changed the title fix: fix multiple electron-updater bugs fix: missing lowercase in electron-updater and invalid file path Mar 27, 2025
@Sytten
Copy link
Contributor Author

Sytten commented Mar 31, 2025

@mmaietta Is this error on mac expected? I didnt really touch that code

@mmaietta
Copy link
Collaborator

mmaietta commented Apr 1, 2025

Retriggered the unit tests and the flaky ones (hdiutil) passed.

Can you please add a changeset to this PR using pnpm generate-changeset?

@Sytten
Copy link
Contributor Author

Sytten commented Apr 1, 2025

Done @mmaietta

@mmaietta mmaietta mentioned this pull request Apr 10, 2025
@mmaietta mmaietta changed the title fix: missing lowercase in electron-updater and invalid file path fix: missing lowercase comparison in electron-updater Apr 11, 2025
@mmaietta mmaietta merged commit 1f50540 into electron-userland:master Apr 11, 2025
19 checks passed
@smileToWxm
Copy link

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.
const updateFileName = getCacheUpdateFileName()
let updateFile = path.join(cacheDir, updateFileName)
So the updateFile here will become C: \ Users \ xxx \ AppData \ Local \ xxxx updater \ pending \ temp- https://xxx.xxx.com/xxx/d0le6oc2v2fgt7i7ejag This format.
I will report
"ENOENT: no such file or directory, open 'C:\Users\xxx\AppData\Local\xxx-updater\pending\temp- https://xxx.xxx.com/xxx/d0le6oc2v2fgt7i7ejag '"
In my opinion, would it be better for everyone to use return path. basename (urlPath) uniformly?

@Sytten
Copy link
Contributor Author

Sytten commented May 20, 2025

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.

@mmaietta
Copy link
Collaborator

@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 electron/* npm packages and to support Electron 36.

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?

there are simpler ways now to do auto-update.

@Sytten
Copy link
Contributor Author

Sytten commented May 20, 2025

For windows (other files are nearby)

Signal: https://github.com/signalapp/Signal-Desktop/blob/07b966eb05ed7d6f4a671be2e9b521af21e67caa/ts/updater/windows.ts#L19

VsCode: https://github.com/microsoft/vscode/blob/f694c69abd029c0fb68f97b9a1e2686afbcbe5cd/src/vs/platform/update/electron-main/updateService.win32.ts

Couple things we might want to try:

  • Use an asar file and patch the files directly
  • Use a signature instead just a SHA to ensure the update is from the developer
  • Support multiple channels (alpha, beta, stable)
  • Make it easier to build a custom providers (currently not trivial)

@mmaietta
Copy link
Collaborator

Oh my indeed, there's a snap updater too! Might be able to bring that over once I peruse the code further
https://github.com/microsoft/vscode/blob/f694c69abd029c0fb68f97b9a1e2686afbcbe5cd/src/vs/platform/update/electron-main/updateService.snap.ts

Use an asar file and patch the files directly

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)

@Sytten
Copy link
Contributor Author

Sytten commented May 21, 2025

@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 package-type, I had to do create electron-publisher-custom.js inside the resources folder

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 app-update.yml also in resources:

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

@smileToWxm
Copy link

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.

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?

@mmaietta
Copy link
Collaborator

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 file: protocol in your app's package.json to point directly to the folder of electron-builder/updater in the submodule checkout. During pnpm/npm/yarn install, it'll resolve the package dependencies since you're pointing to the package folder directly.

@smileToWxm
Copy link

我也是fork之后做了修改,但是不知道怎么在package.json中引用修改后的库,因为fork出来的是整个electron-builder,怎么才能只引用里面的electron-updater呢?

如果您将 fork 用作 git 子模块,则可能只需将依赖项添加到 package.json 中,然后file:在应用程序中使用协议package.json直接指向子模块检出中的 electron-builder/updater 文件夹即可。在pnpm/npm/yarn安装过程中,由于您直接指向了软件包文件夹,因此它将解析软件包依赖关系。

Ok, ok, I'll give it a try on my end. Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants