Compare commits

...

8 Commits

Author SHA1 Message Date
Keeley Hammond
42d7f2783b chore: cherry-pick d5b0cb2acffe from v8 (#50233)
* chore: cherry-pick d5b0cb2acffe from v8

* chore: update patches

---------

Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
2026-03-13 03:11:14 +00:00
Keeley Hammond
61b4c6b93e chore: cherry-pick 248acd90d9a3 from skia (#50234)
* chore: add skia patch dir

* chore: cherry-pick 248acd90d9a3 from skia

* chore: fix the dumbest comma, patch
2026-03-13 03:04:12 +00:00
John Kleinschmidt
b9ca21156b ci: add timeout to test step (#50206)
ci: add timeout to test step (#50186)

Additionally, take a screenshot on timeout so that we can debug why there is a hang
2026-03-12 14:40:26 +01:00
trop[bot]
23960241f9 fix: preserve staged update dir when pruning orphaned updates on macOS (#50217)
* fix: preserve staged update dir when pruning orphaned update dirs on macOS

The previous squirrel.mac patch cleaned up all staged update directories
before starting a new download. This kept disk usage bounded but broke
quitAndInstall() if called while a subsequent checkForUpdates() was in
flight — the already-staged bundle would be deleted out from under it.

This reworks the patch to read ShipItState.plist and preserve the
directory it references, deleting only truly orphaned update.XXXXXXX
directories. Disk footprint stays bounded (at most 2 dirs: staged +
in-progress) and quitAndInstall() remains safe mid-check.

Also adds test coverage for the quitAndInstall/checkForUpdates race and
a triple-stack scenario where 3 updates arrive without a restart.

Refs https://github.com/electron/electron/issues/50200

Co-authored-by: Samuel Attard <sattard@anthropic.com>

* chore: update patches

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Attard <sattard@anthropic.com>
Co-authored-by: Keeley Hammond <khammond@slack-corp.com>
2026-03-12 01:50:13 +00:00
trop[bot]
6d2986302c fix: prevent traffic light buttons flashing on deminiaturize (#50209)
* fix: prevent traffic light buttons flashing on deminiaturize

When a window with a custom `trafficLightPosition` is minimized and
restored, macOS re-layouts the title bar container during the
deminiaturize animation, causing the traffic light buttons to briefly
appear at their default position before being repositioned.

Fix this by hiding the buttons container in `windowWillMiniaturize` and
restoring them (with a redraw to the correct position) in
`windowDidDeminiaturize`.

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* chore: address feedback from review

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-03-11 17:00:10 -04:00
trop[bot]
01b99cd9a9 docs: document Wayland frameless window shadow behaviour (#50195)
docs: update linux frameless window docs to account for hasShadow:false

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: George Xu <george.xu@slack-corp.com>
2026-03-11 15:37:32 -04:00
electron-roller[bot]
a8f64f684f chore: bump chromium to 146.0.7680.72 (41-x-y) (#50196)
chore: bump chromium in DEPS to 146.0.7680.72

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
2026-03-11 15:35:29 -04:00
trop[bot]
ca1b77d9b7 fix: don't call TaskDialogIndirect with disabled parent windows (#50191)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Noah Gregory <noahmgregory@gmail.com>
2026-03-10 18:05:58 -07:00
20 changed files with 1105 additions and 81 deletions

View File

@@ -209,6 +209,7 @@ jobs:
- name: Run Electron Tests
shell: bash
timeout-minutes: 40
env:
MOCHA_REPORTER: mocha-multi-reporters
MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap
@@ -259,6 +260,19 @@ jobs:
fi
fi
- name: Take screenshot on timeout or cancellation
if: ${{ inputs.target-platform != 'linux' && (cancelled() || failure()) }}
shell: bash
run: |
screenshot_dir="src/electron/spec/artifacts"
mkdir -p "$screenshot_dir"
screenshot_file="$screenshot_dir/screenshot-timeout-$(date +%Y%m%d%H%M%S).png"
if [ "${{ inputs.target-platform }}" = "macos" ]; then
screencapture -x "$screenshot_file" || true
elif [ "${{ inputs.target-platform }}" = "win" ]; then
powershell -command "Add-Type -AssemblyName System.Windows.Forms; \$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; \$bitmap = New-Object System.Drawing.Bitmap(\$screen.Width, \$screen.Height); \$graphics = [System.Drawing.Graphics]::FromImage(\$bitmap); \$graphics.CopyFromScreen(\$screen.Location, [System.Drawing.Point]::Empty, \$screen.Size); \$bitmap.Save('$screenshot_file')" || true
fi
- name: Upload Test results to Datadog
env:
DD_ENV: ci
@@ -274,7 +288,7 @@ jobs:
fi
if: always() && !cancelled()
- name: Upload Test Artifacts
if: always() && !cancelled()
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: test_artifacts_${{ env.ARTIFACT_KEY }}_${{ matrix.shard }}

2
DEPS
View File

@@ -2,7 +2,7 @@ gclient_gn_args_from = 'src'
vars = {
'chromium_version':
'146.0.7680.65',
'146.0.7680.72',
'node_version':
'v24.14.0',
'nan_version':

View File

@@ -12,6 +12,10 @@ To create a frameless window, set the [`BaseWindowContructorOptions`][] `frame`
```
On Wayland (Linux), frameless windows have GTK drop shadows and extended
resize boundaries by default. To create a fully frameless window with no
decorations, set `hasShadow: false` in the window constructor options.
## Transparent windows
![Transparent Window](../images/transparent-window.png)

View File

@@ -12,5 +12,6 @@
{ "patch_dir": "src/electron/patches/ReactiveObjC", "repo": "src/third_party/squirrel.mac/vendor/ReactiveObjC" },
{ "patch_dir": "src/electron/patches/webrtc", "repo": "src/third_party/webrtc" },
{ "patch_dir": "src/electron/patches/reclient-configs", "repo": "src/third_party/engflow-reclient-configs" },
{ "patch_dir": "src/electron/patches/skia", "repo": "src/third_party/skia/src" },
{ "patch_dir": "src/electron/patches/sqlite", "repo": "src/third_party/sqlite/src" }
]

1
patches/skia/.patches Normal file
View File

@@ -0,0 +1 @@
cherry-pick-248acd90d9a3.patch

View File

@@ -0,0 +1,539 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Greg Daniel <egdaniel@google.com>
Date: Wed, 11 Mar 2026 16:00:23 -0400
Subject: Make sure we are getting the correct atlas for glyph mask format.
Bug: b/491421267
Change-Id: I4eacd46599eca2df8c10a3fc894b9ce890fae1e2
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1184076
Commit-Queue: Greg Daniel <egdaniel@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
(cherry picked from commit 0cab3e4ee34b3bca6ba7df676639d73ffe4b2135)
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1184916
diff --git a/bench/GlyphQuadFillBench.cpp b/bench/GlyphQuadFillBench.cpp
index 6793512e216b00e1f8112f8e681eecf5beee8fe8..4fd0965185f8bab5a55ec63329bf6aa36ad56ed0 100644
--- a/bench/GlyphQuadFillBench.cpp
+++ b/bench/GlyphQuadFillBench.cpp
@@ -68,7 +68,7 @@ class DirectMaskGlyphVertexFillBenchmark : public Benchmark {
const sktext::gpu::AtlasSubRun* subRun =
sktext::gpu::TextBlobTools::FirstSubRun(fBlob.get());
SkASSERT_RELEASE(subRun);
- subRun->testingOnly_packedGlyphIDToGlyph(&fCache);
+ subRun->testingOnly_packedGlyphIDToGlyph(&fCache, subRun->maskFormat());
fVertices.reset(new char[subRun->vertexStride(drawMatrix) * subRun->glyphCount() * 4]);
}
diff --git a/gn/tests.gni b/gn/tests.gni
index 346cf0ca3b7884d3fb599508d50cc07019322b36..93f4fe5d3281035d1ff469a144e20e00bd783d71 100644
--- a/gn/tests.gni
+++ b/gn/tests.gni
@@ -428,6 +428,7 @@ pathops_tests_sources = [
ganesh_tests_sources = [
"$_tests/AdvancedBlendTest.cpp",
"$_tests/ApplyGammaTest.cpp",
+ "$_tests/AtlasOobTest.cpp",
"$_tests/BackendAllocationTest.cpp",
"$_tests/BackendSurfaceMutableStateTest.cpp",
"$_tests/BlendTest.cpp",
diff --git a/src/gpu/ganesh/text/GrAtlasManager.cpp b/src/gpu/ganesh/text/GrAtlasManager.cpp
index 403bfe274e56293bfe2382b02525ae742ba541a7..1e7d9aa0ce14f19e09d79544730c6aa922ae37d6 100644
--- a/src/gpu/ganesh/text/GrAtlasManager.cpp
+++ b/src/gpu/ganesh/text/GrAtlasManager.cpp
@@ -178,8 +178,7 @@ GrDrawOpAtlas::ErrorCode GrAtlasManager::addGlyphToAtlas(const SkGlyph& skGlyph,
}
SkASSERT(glyph != nullptr);
- MaskFormat glyphFormat = Glyph::FormatFromSkGlyph(skGlyph.maskFormat());
- MaskFormat expectedMaskFormat = this->resolveMaskFormat(glyphFormat);
+ MaskFormat expectedMaskFormat = this->resolveMaskFormat(glyph->fGlyphEntryKey.fFormat);
int bytesPerPixel = MaskFormatBytesPerPixel(expectedMaskFormat);
int padding;
@@ -299,7 +298,7 @@ std::tuple<bool, int> GlyphVector::regenerateAtlasForGanesh(
uint64_t currentAtlasGen = atlasManager->atlasGeneration(maskFormat);
- this->packedGlyphIDToGlyph(target->strikeCache());
+ this->packedGlyphIDToGlyph(target->strikeCache(), maskFormat);
if (fAtlasGeneration != currentAtlasGen) {
// Calculate the texture coordinates for the vertexes during first use (fAtlasGeneration
@@ -316,9 +315,10 @@ std::tuple<bool, int> GlyphVector::regenerateAtlasForGanesh(
for (const Variant& variant : glyphs) {
Glyph* gpuGlyph = variant.glyph;
SkASSERT(gpuGlyph != nullptr);
-
+ SkASSERT(gpuGlyph->fGlyphEntryKey.fFormat == maskFormat);
if (!atlasManager->hasGlyph(maskFormat, gpuGlyph)) {
- const SkGlyph& skGlyph = *metricsAndImages.glyph(gpuGlyph->fPackedID);
+ const SkGlyph& skGlyph =
+ *metricsAndImages.glyph(gpuGlyph->fGlyphEntryKey.fPackedID);
auto code = atlasManager->addGlyphToAtlas(
skGlyph, gpuGlyph, srcPadding, target->resourceProvider(), uploadTarget);
if (code != GrDrawOpAtlas::ErrorCode::kSucceeded) {
diff --git a/src/gpu/graphite/Device.cpp b/src/gpu/graphite/Device.cpp
index ba8049369956f5ea00e09f947c4ed9067a99bb6c..1e7027ebcfaaf79e2c6bd17b65a046bdb8eef9ae 100644
--- a/src/gpu/graphite/Device.cpp
+++ b/src/gpu/graphite/Device.cpp
@@ -1436,6 +1436,7 @@ void Device::drawAtlasSubRun(const sktext::gpu::AtlasSubRun* subRun,
int padding) {
return glyphs->regenerateAtlasForGraphite(begin, end, maskFormat, padding, fRecorder);
};
+
for (int subRunCursor = 0; subRunCursor < subRunEnd;) {
// For the remainder of the run, add any atlas uploads to the Recorder's TextAtlasManager
auto[ok, glyphsRegenerated] = subRun->regenerateAtlas(subRunCursor, subRunEnd,
diff --git a/src/gpu/graphite/text/TextAtlasManager.cpp b/src/gpu/graphite/text/TextAtlasManager.cpp
index 6602a76c150bff077666fb91b990d3e45d528ce2..cbb51a66846922995912c3159afba879a2487313 100644
--- a/src/gpu/graphite/text/TextAtlasManager.cpp
+++ b/src/gpu/graphite/text/TextAtlasManager.cpp
@@ -207,8 +207,7 @@ DrawAtlas::ErrorCode TextAtlasManager::addGlyphToAtlas(const SkGlyph& skGlyph,
}
SkASSERT(glyph != nullptr);
- MaskFormat glyphFormat = Glyph::FormatFromSkGlyph(skGlyph.maskFormat());
- MaskFormat expectedMaskFormat = this->resolveMaskFormat(glyphFormat);
+ MaskFormat expectedMaskFormat = this->resolveMaskFormat(glyph->fGlyphEntryKey.fFormat);
int bytesPerPixel = MaskFormatBytesPerPixel(expectedMaskFormat);
int padding;
@@ -359,7 +358,7 @@ std::tuple<bool, int> GlyphVector::regenerateAtlasForGraphite(int begin,
uint64_t currentAtlasGen = atlasManager->atlasGeneration(maskFormat);
- this->packedGlyphIDToGlyph(recorder->priv().strikeCache());
+ this->packedGlyphIDToGlyph(recorder->priv().strikeCache(), maskFormat);
if (fAtlasGeneration != currentAtlasGen) {
// Calculate the texture coordinates for the vertexes during first use (fAtlasGeneration
@@ -375,9 +374,10 @@ std::tuple<bool, int> GlyphVector::regenerateAtlasForGraphite(int begin,
for (const Variant& variant : glyphs) {
Glyph* gpuGlyph = variant.glyph;
SkASSERT(gpuGlyph != nullptr);
-
+ SkASSERT(gpuGlyph->fGlyphEntryKey.fFormat == maskFormat);
if (!atlasManager->hasGlyph(maskFormat, gpuGlyph)) {
- const SkGlyph& skGlyph = *metricsAndImages.glyph(gpuGlyph->fPackedID);
+ const SkGlyph& skGlyph =
+ *metricsAndImages.glyph(gpuGlyph->fGlyphEntryKey.fPackedID);
auto code = atlasManager->addGlyphToAtlas(skGlyph, gpuGlyph, srcPadding);
if (code != DrawAtlas::ErrorCode::kSucceeded) {
success = code != DrawAtlas::ErrorCode::kError;
diff --git a/src/text/gpu/Glyph.h b/src/text/gpu/Glyph.h
index 821612d68cecfe9dae9518e376e09fdf233395ad..7942006a563bcab925ea2129ab6f6beea438a4c8 100644
--- a/src/text/gpu/Glyph.h
+++ b/src/text/gpu/Glyph.h
@@ -14,6 +14,25 @@
namespace sktext::gpu {
+struct GlyphEntryKey {
+ explicit GlyphEntryKey(SkPackedGlyphID id, skgpu::MaskFormat format)
+ : fPackedID(id), fFormat(format) {}
+
+ const SkPackedGlyphID fPackedID;
+ skgpu::MaskFormat fFormat;
+
+ bool operator==(const GlyphEntryKey& that) const {
+ return fPackedID == that.fPackedID && fFormat == that.fFormat;
+ }
+ bool operator!=(const GlyphEntryKey& that) const {
+ return !(*this == that);
+ }
+
+ uint32_t hash() const {
+ return fPackedID.hash();
+ }
+};
+
class Glyph {
public:
static skgpu::MaskFormat FormatFromSkGlyph(SkMask::Format format) {
@@ -34,10 +53,11 @@ public:
SkUNREACHABLE;
}
- explicit Glyph(SkPackedGlyphID packedGlyphID) : fPackedID(packedGlyphID) {}
+ explicit Glyph(SkPackedGlyphID packedGlyphID, skgpu::MaskFormat format)
+ : fGlyphEntryKey(packedGlyphID, format) {}
- const SkPackedGlyphID fPackedID;
- skgpu::AtlasLocator fAtlasLocator;
+ const GlyphEntryKey fGlyphEntryKey;
+ skgpu::AtlasLocator fAtlasLocator;
};
} // namespace sktext::gpu
diff --git a/src/text/gpu/GlyphVector.cpp b/src/text/gpu/GlyphVector.cpp
index 2a8e85f926aa547169f4b85372e9d3fb99816956..7bec7a0b77d8560d5ef978281edd7df6c45cb56f 100644
--- a/src/text/gpu/GlyphVector.cpp
+++ b/src/text/gpu/GlyphVector.cpp
@@ -99,14 +99,14 @@ SkSpan<const Glyph*> GlyphVector::glyphs() const {
// packedGlyphIDToGlyph must be run in single-threaded mode.
// If fSkStrike is not sk_sp<SkStrike> then the conversion to Glyph* has not happened.
-void GlyphVector::packedGlyphIDToGlyph(StrikeCache* cache) {
+void GlyphVector::packedGlyphIDToGlyph(StrikeCache* cache, MaskFormat maskFormat) {
if (fTextStrike == nullptr) {
SkStrike* strike = fStrikePromise.strike();
fTextStrike = cache->findOrCreateStrike(strike->strikeSpec());
// Get all the atlas locations for each glyph.
for (Variant& variant : fGlyphs) {
- variant.glyph = fTextStrike->getGlyph(variant.packedGlyphID);
+ variant.glyph = fTextStrike->getGlyph(variant.packedGlyphID, maskFormat);
}
// This must be pinned for the Atlas filling to work.
diff --git a/src/text/gpu/GlyphVector.h b/src/text/gpu/GlyphVector.h
index 42b92a93f70cc6d86d0a87dd07c2244e0da1281c..1eec6327d38fb4472b027faae68eecb9ad7509d7 100644
--- a/src/text/gpu/GlyphVector.h
+++ b/src/text/gpu/GlyphVector.h
@@ -68,7 +68,7 @@ public:
// the sub runs.
int unflattenSize() const { return GlyphVectorSize(fGlyphs.size()); }
- void packedGlyphIDToGlyph(StrikeCache* cache);
+ void packedGlyphIDToGlyph(StrikeCache* cache, skgpu::MaskFormat);
static size_t GlyphVectorSize(size_t count) {
return sizeof(Variant) * count;
diff --git a/src/text/gpu/StrikeCache.cpp b/src/text/gpu/StrikeCache.cpp
index add3127c92fdbfe56d6b56209a2235ce5a9f5acb..19df48329fd500f8682669ec96eb883b58243fdd 100644
--- a/src/text/gpu/StrikeCache.cpp
+++ b/src/text/gpu/StrikeCache.cpp
@@ -207,10 +207,11 @@ TextStrike::TextStrike(StrikeCache* strikeCache, const SkStrikeSpec& strikeSpec)
: fStrikeCache(strikeCache)
, fStrikeSpec{strikeSpec} {}
-Glyph* TextStrike::getGlyph(SkPackedGlyphID packedGlyphID) {
- Glyph* glyph = fCache.findOrNull(packedGlyphID);
+Glyph* TextStrike::getGlyph(SkPackedGlyphID packedGlyphID, skgpu::MaskFormat format) {
+ GlyphEntryKey localKey(packedGlyphID, format);
+ Glyph* glyph = fCache.findOrNull(localKey);
if (glyph == nullptr) {
- glyph = fAlloc.make<Glyph>(packedGlyphID);
+ glyph = fAlloc.make<Glyph>(packedGlyphID, format);
fCache.set(glyph);
fMemoryUsed += sizeof(Glyph);
if (!fRemoved) {
@@ -220,11 +221,11 @@ Glyph* TextStrike::getGlyph(SkPackedGlyphID packedGlyphID) {
return glyph;
}
-const SkPackedGlyphID& TextStrike::HashTraits::GetKey(const Glyph* glyph) {
- return glyph->fPackedID;
+const GlyphEntryKey& TextStrike::HashTraits::GetKey(const Glyph* glyph) {
+ return glyph->fGlyphEntryKey;
}
-uint32_t TextStrike::HashTraits::Hash(SkPackedGlyphID key) {
+uint32_t TextStrike::HashTraits::Hash(GlyphEntryKey key) {
return key.hash();
}
diff --git a/src/text/gpu/StrikeCache.h b/src/text/gpu/StrikeCache.h
index 007c45c6c6feecba3ff031ba3939ad2402e082b9..014afd5286602e3e049d8e48ae328273e599dc41 100644
--- a/src/text/gpu/StrikeCache.h
+++ b/src/text/gpu/StrikeCache.h
@@ -13,6 +13,7 @@
#include "src/core/SkDescriptor.h"
#include "src/core/SkStrikeSpec.h"
#include "src/core/SkTHash.h"
+#include "src/gpu/AtlasTypes.h"
#include <cstddef>
#include <cstdint>
@@ -32,6 +33,7 @@ struct SkPackedGlyphID;
namespace sktext::gpu {
class Glyph;
+struct GlyphEntryKey;
class StrikeCache;
// The TextStrike manages an SkArenaAlloc for Glyphs. The SkStrike is what actually creates
@@ -43,7 +45,7 @@ public:
TextStrike(StrikeCache* strikeCache,
const SkStrikeSpec& strikeSpec);
- Glyph* getGlyph(SkPackedGlyphID);
+ Glyph* getGlyph(SkPackedGlyphID, skgpu::MaskFormat format);
const SkStrikeSpec& strikeSpec() const { return fStrikeSpec; }
const SkDescriptor& getDescriptor() const { return fStrikeSpec.descriptor(); }
@@ -54,11 +56,11 @@ private:
const SkStrikeSpec fStrikeSpec;
struct HashTraits {
- static const SkPackedGlyphID& GetKey(const Glyph* glyph);
- static uint32_t Hash(SkPackedGlyphID key);
+ static const GlyphEntryKey& GetKey(const Glyph* glyph);
+ static uint32_t Hash(GlyphEntryKey key);
};
// Map SkPackedGlyphID -> Glyph*.
- skia_private::THashTable<Glyph*, SkPackedGlyphID, HashTraits> fCache;
+ skia_private::THashTable<Glyph*, GlyphEntryKey, HashTraits> fCache;
// Store for the glyph information.
SkArenaAlloc fAlloc{512};
diff --git a/src/text/gpu/SubRunContainer.cpp b/src/text/gpu/SubRunContainer.cpp
index 34b3f347aa404a964f06ff18d43eef99f3b25f6b..66531e030ddfabc2204c8e27c9478251c507b5ec 100644
--- a/src/text/gpu/SubRunContainer.cpp
+++ b/src/text/gpu/SubRunContainer.cpp
@@ -651,8 +651,9 @@ public:
int glyphSrcPadding() const override { return 0; }
- void testingOnly_packedGlyphIDToGlyph(StrikeCache* cache) const override {
- fGlyphs.packedGlyphIDToGlyph(cache);
+ void testingOnly_packedGlyphIDToGlyph(StrikeCache* cache,
+ skgpu::MaskFormat maskFormat) const override {
+ fGlyphs.packedGlyphIDToGlyph(cache, maskFormat);
}
std::tuple<bool, SkRect> deviceRectAndNeedsTransform(
@@ -755,8 +756,9 @@ public:
const AtlasSubRun* testingOnly_atlasSubRun() const override { return this; }
- void testingOnly_packedGlyphIDToGlyph(StrikeCache *cache) const override {
- fGlyphs.packedGlyphIDToGlyph(cache);
+ void testingOnly_packedGlyphIDToGlyph(StrikeCache *cache,
+ skgpu::MaskFormat maskFormat) const override {
+ fGlyphs.packedGlyphIDToGlyph(cache, maskFormat);
}
int glyphSrcPadding() const override { return 1; }
@@ -893,8 +895,9 @@ public:
const AtlasSubRun* testingOnly_atlasSubRun() const override { return this; }
- void testingOnly_packedGlyphIDToGlyph(StrikeCache *cache) const override {
- fGlyphs.packedGlyphIDToGlyph(cache);
+ void testingOnly_packedGlyphIDToGlyph(StrikeCache *cache,
+ skgpu::MaskFormat maskFormat) const override {
+ fGlyphs.packedGlyphIDToGlyph(cache, maskFormat);
}
int glyphSrcPadding() const override { return SK_DistanceFieldInset; }
diff --git a/src/text/gpu/SubRunContainer.h b/src/text/gpu/SubRunContainer.h
index 2573dbb3964e9ab2cc0e276b60d4ab4f9804f0d9..4d1a3c8c2d55015d3d351d322ef039c45be2a398 100644
--- a/src/text/gpu/SubRunContainer.h
+++ b/src/text/gpu/SubRunContainer.h
@@ -167,7 +167,7 @@ public:
const VertexFiller& vertexFiller() const { return fVertexFiller; }
- virtual void testingOnly_packedGlyphIDToGlyph(StrikeCache* cache) const = 0;
+ virtual void testingOnly_packedGlyphIDToGlyph(StrikeCache* cache, skgpu::MaskFormat) const = 0;
protected:
const VertexFiller fVertexFiller;
diff --git a/tests/AtlasOobTest.cpp b/tests/AtlasOobTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4e6fb02ee6af6543df285d8112f1a2ced5bd9ac9
--- /dev/null
+++ b/tests/AtlasOobTest.cpp
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#include "include/core/SkCanvas.h"
+#include "include/core/SkGraphics.h"
+#include "include/core/SkSerialProcs.h"
+#include "include/core/SkSurface.h"
+#include "include/private/chromium/SkChromeRemoteGlyphCache.h"
+#include "include/private/chromium/Slug.h"
+#include "src/core/SkDescriptor.h"
+#include "src/core/SkReadBuffer.h"
+#include "src/core/SkTypeface_remote.h"
+#include "src/core/SkWriteBuffer.h"
+#include "src/gpu/AtlasTypes.h"
+#include "tests/CtsEnforcement.h"
+#include "tests/Test.h"
+#include "tools/ToolUtils.h"
+
+#if defined(SK_GANESH)
+#include "include/gpu/ganesh/GrDirectContext.h"
+#include "include/gpu/ganesh/SkSurfaceGanesh.h"
+#endif
+
+#if defined(SK_GRAPHITE)
+#include "include/gpu/graphite/Context.h"
+#include "include/gpu/graphite/Surface.h"
+#include "tools/graphite/GraphiteTestContext.h"
+#endif // defined(SK_GRAPHITE)
+
+#include <vector>
+#include <cstring>
+
+namespace {
+class FakeDiscardableManager : public SkStrikeClient::DiscardableHandleManager {
+public:
+ bool deleteHandle(SkDiscardableHandleId) override { return false; }
+ void notifyCacheMiss(SkStrikeClient::CacheMissType, int) override {}
+ void notifyReadFailure(const ReadFailureData&) override {}
+ void assertHandleValid(SkDiscardableHandleId) override {}
+};
+
+unsigned char kStrikeData[] = {
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x07, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65,
+ 0x00, 0x00, 0x00, 0x65, 0xd8, 0x50, 0xda, 0x99, 0x4c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x63, 0x65, 0x72, 0x73, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x80, 0x41,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x41,
+ 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x41, 0x00, 0x00, 0x00, 0x00,
+ 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x41, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x64, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x20, 0x41, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x41,
+ 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x41, 0x00, 0x00, 0x00, 0x00,
+ 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x41, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x66, 0x86, 0x07, 0xc2, 0x42,
+ 0x4c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x63, 0x65, 0x72, 0x73, 0x38, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x80, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x99, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x41, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+};
+
+unsigned char kDrawSlugOp[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x41,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f,
+ 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41,
+ 0x00, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x86, 0x07, 0xc2, 0x42, 0x4c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x63, 0x65, 0x72, 0x73,
+ 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x80, 0x41, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x99, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+} // namespace
+
+// TODO: We expect this test to correctly hit an SkUnreachable and then crash. That does not work
+// with our current testing framework because we have no to "expect" a crash. So for now we will
+// land this test with only the valid loop enabled, but to test this is working locally, you should
+// change the loop to have both iterations.
+static void run_atlas_oob_test(skiatest::Reporter* reporter, SkCanvas* canvas) {
+ auto discardableManager = sk_make_sp<FakeDiscardableManager>();
+ SkStrikeClient client(discardableManager, false);
+
+ // 1. Prepare Strike Data
+ if (!client.readStrikeData(kStrikeData, sizeof(kStrikeData))) {
+ REPORTER_ASSERT(reporter, false, "Failed to read initial strike data");
+ }
+
+ // 2. Prepare and Execute DrawSlug ops
+ SkPaint paint;
+ for (int idx = 0; idx < 1; ++idx) {
+// for (int idx = 0; idx < 2; ++idx) {
+ if (idx == 0) {
+ kDrawSlugOp[0x48] = (unsigned char)skgpu::MaskFormat::kARGB;
+ } else if (idx == 1) {
+ kDrawSlugOp[0x48] = (unsigned char)skgpu::MaskFormat::kA8;
+ }
+ kDrawSlugOp[0xd8] = SkMask::kARGB32_Format;
+ kDrawSlugOp[0xe0] = 0x99;
+
+ auto slug = client.deserializeSlugForTest(kDrawSlugOp, sizeof(kDrawSlugOp));
+ if (slug) {
+ slug->draw(canvas, paint);
+ }
+ }
+
+}
+
+#if defined(SK_GANESH)
+DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(Atlas_Oob_ganesh, reporter, ctxInfo, CtsEnforcement::kNextRelease) {
+ auto dContext = ctxInfo.directContext();
+ SkImageInfo info = SkImageInfo::MakeN32Premul(1024, 1024);
+ auto surface = SkSurfaces::RenderTarget(dContext, skgpu::Budgeted::kNo, info);
+ if (!surface) return;
+ auto canvas = surface->getCanvas();
+
+ run_atlas_oob_test(reporter, canvas);
+
+ dContext->flushAndSubmit();
+}
+#endif // defined(SK_GANESH)
+
+#if defined(SK_GRAPHITE)
+DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(Atlas_Oob_graphite, reporter, context, CtsEnforcement::kNextRelease) {
+ using namespace skgpu::graphite;
+ std::unique_ptr<Recorder> recorder = context->makeRecorder();
+ SkImageInfo info = SkImageInfo::MakeN32Premul(1024, 1024);
+ auto surface = SkSurfaces::RenderTarget(recorder.get(), info);
+ if (!surface) return;
+ auto canvas = surface->getCanvas();
+
+ run_atlas_oob_test(reporter, canvas);
+
+ std::unique_ptr<Recording> recording = recorder->snap();
+ InsertRecordingInfo recordingInfo;
+ recordingInfo.fRecording = recording.get();
+ context->insertRecording(recordingInfo);
+ context->submit();
+}
+#endif // defined(SK_GRAPHITE)

View File

@@ -9,4 +9,4 @@ refactor_use_non-deprecated_nskeyedarchiver_apis.patch
chore_turn_off_launchapplicationaturl_deprecation_errors_in_squirrel.patch
fix_crash_when_process_to_extract_zip_cannot_be_launched.patch
use_uttype_class_instead_of_deprecated_uttypeconformsto.patch
fix_clean_up_old_staged_updates_before_downloading_new_update.patch
fix_clean_up_orphaned_staged_updates_before_downloading_new_update.patch

View File

@@ -1,64 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andy Locascio <loc@anthropic.com>
Date: Tue, 6 Jan 2026 08:23:03 -0800
Subject: fix: clean up old staged updates before downloading new update
When checkForUpdates() is called while an update is already staged,
Squirrel creates a new temporary directory for the download without
cleaning up the old one. This can lead to significant disk usage if
the app keeps checking for updates without restarting.
This change adds a force parameter to pruneUpdateDirectories that
bypasses the AwaitingRelaunch state check. This is called before
creating a new temp directory, ensuring old staged updates are
cleaned up when a new download starts.
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
index d156616e81e6f25a3bded30e6216b8fc311f31bc..6cd4346bf43b191147aff819cb93387e71275a46 100644
--- a/Squirrel/SQRLUpdater.m
+++ b/Squirrel/SQRLUpdater.m
@@ -543,11 +543,17 @@ - (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL
#pragma mark File Management
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
- return [[[RACSignal
+ // Clean up any old staged update directories before creating a new one.
+ // This prevents disk usage from growing when checkForUpdates() is called
+ // multiple times without the app restarting.
+ return [[[[[self
+ pruneUpdateDirectoriesWithForce:YES]
+ ignoreValues]
+ concat:[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
- }]
+ }]]
flattenMap:^(NSURL *storageURL) {
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@@ -643,7 +649,7 @@ - (BOOL)isRunningOnReadOnlyVolume {
- (RACSignal *)performHousekeeping {
return [[RACSignal
- merge:@[ [self pruneUpdateDirectories], [self truncateLogs] ]]
+ merge:@[ [self pruneUpdateDirectoriesWithForce:NO], [self truncateLogs] ]]
catch:^(NSError *error) {
NSLog(@"Error doing housekeeping: %@", error);
return [RACSignal empty];
@@ -658,11 +664,12 @@ - (RACSignal *)performHousekeeping {
///
/// Sends each removed directory then completes, or errors, on an unspecified
/// thread.
-- (RACSignal *)pruneUpdateDirectories {
+- (RACSignal *)pruneUpdateDirectoriesWithForce:(BOOL)force {
return [[[RACSignal
defer:^{
- // If we already have updates downloaded we don't wanna prune them.
- if (self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
+ // If we already have updates downloaded we don't wanna prune them,
+ // unless force is YES (used when starting a new download).
+ if (!force && self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];

View File

@@ -0,0 +1,130 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andy Locascio <loc@anthropic.com>
Date: Tue, 6 Jan 2026 08:23:03 -0800
Subject: fix: clean up orphaned staged updates before downloading new update
When checkForUpdates() is called while an update is already staged,
Squirrel creates a new temporary directory for the download without
cleaning up the old one. This can lead to significant disk usage if
the app keeps checking for updates without restarting.
This change adds a pruneOrphanedUpdateDirectories step before creating
a new temp directory. Unlike a blanket prune, this reads the current
ShipItState.plist and preserves the directory it references, deleting
only truly orphaned update directories. This keeps the on-disk
footprint bounded (at most 2 dirs) while ensuring quitAndInstall
remains safe to call even when a new check is in progress.
Refs https://github.com/electron/electron/issues/50200
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
index d156616e81e6f25a3bded30e6216b8fc311f31bc..41856e5754228d33982db72f97f2ff241615a357 100644
--- a/Squirrel/SQRLUpdater.m
+++ b/Squirrel/SQRLUpdater.m
@@ -543,11 +543,19 @@ - (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL
#pragma mark File Management
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
- return [[[RACSignal
+ // Clean up any orphaned update directories before creating a new one.
+ // This prevents disk usage from growing when checkForUpdates() is called
+ // multiple times without the app restarting. The currently staged update
+ // (referenced by ShipItState.plist) is always preserved so quitAndInstall
+ // remains safe to call while a new check is in progress.
+ return [[[[[self
+ pruneOrphanedUpdateDirectories]
+ ignoreValues]
+ concat:[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
- }]
+ }]]
flattenMap:^(NSURL *storageURL) {
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@@ -668,25 +676,68 @@ - (RACSignal *)pruneUpdateDirectories {
return [directoryManager storageURL];
}]
flattenMap:^(NSURL *storageURL) {
- NSFileManager *manager = [[NSFileManager alloc] init];
- NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
- NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
- return YES;
- }];
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:nil];
+ }]
+ setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
+}
- return [[enumerator.rac_sequence.signal
- filter:^(NSURL *enumeratedURL) {
- NSString *name = enumeratedURL.lastPathComponent;
- return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
- }]
- doNext:^(NSURL *directoryURL) {
- NSError *error = nil;
- if (![manager removeItemAtURL:directoryURL error:&error]) {
- NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
- }
+/// Lazily removes orphaned temporary directories upon subscription, always
+/// preserving the directory currently referenced by ShipItState.plist so that
+/// quitAndInstall remains safe to call mid-check.
+///
+/// Safe to call in any state. Sends each removed directory then completes on
+/// an unspecified thread. Errors reading the staged request are swallowed
+/// (treated as "nothing staged").
+- (RACSignal *)pruneOrphanedUpdateDirectories {
+ return [[[[[SQRLShipItRequest
+ readUsingURL:self.shipItStateURL]
+ map:^(SQRLShipItRequest *request) {
+ // The request holds the URL to the staged .app bundle; its parent
+ // is the update.XXXXXXX directory we must preserve.
+ return [request.updateBundleURL URLByDeletingLastPathComponent];
+ }]
+ catch:^(NSError *error) {
+ // No staged request (or unreadable) — nothing to preserve.
+ return [RACSignal return:nil];
+ }]
+ flattenMap:^(NSURL *stagedDirectoryURL) {
+ SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
+ return [[directoryManager storageURL]
+ flattenMap:^(NSURL *storageURL) {
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:stagedDirectoryURL];
}];
}]
- setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
+ setNameWithFormat:@"%@ -pruneOrphanedUpdateDirectories", self];
+}
+
+/// Shared enumerate-and-delete logic for update temp directories.
+///
+/// storageURL - The Squirrel storage root to enumerate. Must not be nil.
+/// excludedURL - Directory to skip (compared by standardized path). May be nil.
+- (RACSignal *)removeUpdateDirectoriesInStorageURL:(NSURL *)storageURL excludingURL:(NSURL *)excludedURL {
+ NSParameterAssert(storageURL != nil);
+
+ NSFileManager *manager = [[NSFileManager alloc] init];
+ NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
+ NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
+ return YES;
+ }];
+
+ NSString *excludedPath = excludedURL.URLByStandardizingPath.path;
+
+ return [[enumerator.rac_sequence.signal
+ filter:^(NSURL *enumeratedURL) {
+ NSString *name = enumeratedURL.lastPathComponent;
+ if (![name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix]) return NO;
+ if (excludedPath != nil && [enumeratedURL.URLByStandardizingPath.path isEqualToString:excludedPath]) return NO;
+ return YES;
+ }]
+ doNext:^(NSURL *directoryURL) {
+ NSError *error = nil;
+ if (![manager removeItemAtURL:directoryURL error:&error]) {
+ NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
+ }
+ }];
}

View File

@@ -1 +1,2 @@
chore_allow_customizing_microtask_policy_per_context.patch
cherry-pick-d5b0cb2acffe.patch

View File

@@ -0,0 +1,50 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Wed, 25 Feb 2026 12:56:18 +0100
Subject: [M144 Merge] [maglev] fix CanElideWriteBarrier Smi recording for phis
Recording a Tagged use is not enough for 2 reasons:
* Tagged uses are sometimes ignored, in particular for loop phis
where we distinguish in-loop and out-of-loop uses.
* This Tagged use could only prevent untagging of this specific phi,
but none of its inputs. So we could have a Smi phi as input to the
current phi which gets untagged and retagged to a non-Smi, all
while the current phi doesn't get untagged.
(cherry picked from commit a54bf5cd45e5b119e2afe6019428e81c3d626fb3)
Change-Id: I9b3a2ea339f2c9d81dbb74a44425ba55d8c73871
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7604255
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Darius Mercadier <dmercadier@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#105444}
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7659106
Auto-Submit: Srinivas Sista <srinivassista@chromium.org>
Reviewed-by: Rezvan Mahdavi Hezaveh <rezvan@chromium.org>
Commit-Queue: Srinivas Sista <srinivassista@chromium.org>
Reviewed-by: Deepti Gandluri <gdeepti@chromium.org>
Owners-Override: Srinivas Sista <srinivassista@chromium.org>
Cr-Commit-Position: refs/branch-heads/14.4@{#64}
Cr-Branched-From: 80acc26727d5a34e77dabeebe7c9213ec1bd4768-refs/heads/14.4.258@{#1}
Cr-Branched-From: ce7e597e90f6df3fa4b6df224bc613b80c635450-refs/heads/main@{#104020}
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index e334c256779f224d66e2ec3c9bac2ecd88f6708f..e93c52ee51f3e80ce24af622f74ae3635446f6e9 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -4458,7 +4458,11 @@ bool MaglevGraphBuilder::CanElideWriteBarrier(ValueNode* object,
ValueNode* value) {
if (value->Is<RootConstant>() || value->Is<ConsStringMap>()) return true;
if (!IsEmptyNodeType(GetType(value)) && CheckType(value, NodeType::kSmi)) {
- value->MaybeRecordUseReprHint(UseRepresentation::kTagged);
+ if constexpr (SmiValuesAre31Bits()) {
+ if (Phi* value_as_phi = value->TryCast<Phi>()) {
+ value_as_phi->SetUseRequires31BitValue();
+ }
+ }
return true;
}

View File

@@ -170,6 +170,12 @@ class NativeWindowMac : public NativeWindow,
void NotifyWindowDidFailToEnterFullScreen();
void NotifyWindowWillLeaveFullScreen();
// Hide/show traffic light buttons around miniaturize/deminiaturize to
// prevent them from flashing at the default position during the restore
// animation when a custom trafficLightPosition is configured.
void HideTrafficLights();
void RestoreTrafficLights();
// Cleanup observers when window is getting closed. Note that the destructor
// can be called much later after window gets closed, so we should not do
// cleanup in destructor.

View File

@@ -1533,6 +1533,18 @@ void NativeWindowMac::RedrawTrafficLights() {
[buttons_proxy_ redraw];
}
void NativeWindowMac::HideTrafficLights() {
if (buttons_proxy_)
[buttons_proxy_ setVisible:NO];
}
void NativeWindowMac::RestoreTrafficLights() {
if (buttons_proxy_ && window_button_visibility_.value_or(true)) {
[buttons_proxy_ redraw];
[buttons_proxy_ setVisible:YES];
}
}
// In simpleFullScreen mode, update the frame for new bounds.
void NativeWindowMac::UpdateFrame() {
NSWindow* window = GetNativeWindow().GetNativeNSWindow();

View File

@@ -256,6 +256,10 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
shell_->SetWindowLevel(NSNormalWindowLevel);
shell_->UpdateWindowOriginalFrame();
shell_->DetachChildren();
// Hide the traffic light buttons container before miniaturize so that
// when the window is restored, macOS does not render the buttons at
// their default position during the deminiaturize animation.
shell_->HideTrafficLights();
}
- (void)windowDidMiniaturize:(NSNotification*)notification {
@@ -273,6 +277,10 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
shell_->set_wants_to_be_visible(true);
shell_->AttachChildren();
shell_->SetWindowLevel(level_);
// Reposition traffic light buttons and make them visible again.
// They were hidden in windowWillMiniaturize to prevent a flash at
// the default (0,0) position during the restore animation.
shell_->RestoreTrafficLights();
shell_->NotifyWindowRestore();
}

View File

@@ -158,7 +158,7 @@ DialogResult ShowTaskDialogWstr(gfx::AcceleratedWidget parent,
config.hInstance = GetModuleHandle(nullptr);
config.dwFlags = flags;
if (parent) {
if (parent && ::IsWindowEnabled(parent)) {
config.hwndParent = parent;
config.dwFlags |= TDF_POSITION_RELATIVE_TO_WINDOW;
}

View File

@@ -403,7 +403,7 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
});
});
it('should clean up old staged update directories when a new update is downloaded', async () => {
it('should preserve the staged update directory and prune orphaned ones when a new update is downloaded', async () => {
// Clean up any existing update directories before the test
await cleanSquirrelCache();
@@ -419,16 +419,23 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
}, async (_, updateZipPath3) => {
let updateCount = 0;
let downloadCount = 0;
let directoriesDuringSecondDownload: string[] = [];
let dirsDuringFirstDownload: string[] = [];
let dirsDuringSecondDownload: string[] = [];
server.get('/update-file', async (req, res) => {
downloadCount++;
// When the second download request arrives, Squirrel has already
// called uniqueTemporaryDirectoryForUpdate which (with our patch)
// cleans up old directories before creating the new one.
// Without the patch, both directories would exist at this point.
if (downloadCount === 2) {
directoriesDuringSecondDownload = await getUpdateDirectoriesInCache();
// Snapshot update directories at the moment each download begins.
// By this point uniqueTemporaryDirectoryForUpdate has already run
// (prune + mkdtemp). We want to verify:
// 1st download: 1 dir (nothing to preserve, nothing to prune)
// 2nd download: 2 dirs (staged dir from 1st check is preserved
// so quitAndInstall stays safe, + new temp dir)
// The count never exceeds 2 across repeated checks — orphaned dirs
// (no longer referenced by ShipItState.plist) get pruned.
if (downloadCount === 1) {
dirsDuringFirstDownload = await getUpdateDirectoriesInCache();
} else if (downloadCount === 2) {
dirsDuringSecondDownload = await getUpdateDirectoriesInCache();
}
res.download(updateCount > 1 ? updateZipPath3 : updateZipPath2);
});
@@ -455,15 +462,181 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
await relaunchPromise;
// During the second download, the old staged update directory should
// have been cleaned up. With our patch, there should be exactly 1
// directory (the new one). Without the patch, there would be 2.
expect(directoriesDuringSecondDownload).to.have.lengthOf(1,
`Expected 1 update directory during second download but found ${directoriesDuringSecondDownload.length}: ${directoriesDuringSecondDownload.join(', ')}`);
// First download: exactly one temp dir (the first update).
expect(dirsDuringFirstDownload).to.have.lengthOf(1,
`Expected 1 update directory during first download but found ${dirsDuringFirstDownload.length}: ${dirsDuringFirstDownload.join(', ')}`);
// Second download: exactly two — the staged one preserved + the new
// one. Crucially the first download's directory must still be present,
// otherwise a mid-download quitAndInstall would find a dangling
// ShipItState.plist.
expect(dirsDuringSecondDownload).to.have.lengthOf(2,
`Expected 2 update directories during second download (staged + new) but found ${dirsDuringSecondDownload.length}: ${dirsDuringSecondDownload.join(', ')}`);
expect(dirsDuringSecondDownload).to.include(dirsDuringFirstDownload[0],
'The staged update directory from the first download must be preserved during the second download');
});
});
});
it('should keep the update directory count bounded across repeated checks', async () => {
// Verifies the orphan prune actually fires: after a second download
// completes and rewrites ShipItState.plist, the first directory is no
// longer referenced and must be removed when a third check begins.
// Without this, directories would accumulate forever.
await cleanSquirrelCache();
await withUpdatableApp({
nextVersion: '2.0.0',
startFixture: 'update-triple-stack',
endFixture: 'update-triple-stack'
}, async (appPath, updateZipPath2) => {
await withUpdatableApp({
nextVersion: '3.0.0',
startFixture: 'update-triple-stack',
endFixture: 'update-triple-stack'
}, async (_, updateZipPath3) => {
await withUpdatableApp({
nextVersion: '4.0.0',
startFixture: 'update-triple-stack',
endFixture: 'update-triple-stack'
}, async (__, updateZipPath4) => {
let downloadCount = 0;
const dirsPerDownload: string[][] = [];
server.get('/update-file', async (req, res) => {
downloadCount++;
// Snapshot after prune+mkdtemp but before the payload transfers.
dirsPerDownload.push(await getUpdateDirectoriesInCache());
const zips = [updateZipPath2, updateZipPath3, updateZipPath4];
res.download(zips[Math.min(downloadCount, zips.length) - 1]);
});
server.get('/update-check', (req, res) => {
res.json({
url: `http://localhost:${port}/update-file`,
name: 'My Release Name',
notes: 'Theses are some release notes innit',
pub_date: (new Date()).toString()
});
});
const relaunchPromise = new Promise<void>((resolve) => {
server.get('/update-check/updated/:version', (req, res) => {
res.status(204).send();
resolve();
});
});
const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult).to.have.property('code', 0);
expect(launchResult.out).to.include('Update Downloaded');
});
await relaunchPromise;
expect(requests[requests.length - 1].url).to.equal('/update-check/updated/4.0.0');
expect(dirsPerDownload).to.have.lengthOf(3);
// 1st: fresh cache, 1 dir.
expect(dirsPerDownload[0]).to.have.lengthOf(1,
`1st download: ${dirsPerDownload[0].join(', ')}`);
// 2nd: staged (1st) preserved + new = 2 dirs.
expect(dirsPerDownload[1]).to.have.lengthOf(2,
`2nd download: ${dirsPerDownload[1].join(', ')}`);
expect(dirsPerDownload[1]).to.include(dirsPerDownload[0][0]);
// 3rd: 1st is now orphaned (plist points to 2nd) — must be pruned.
// Staged (2nd) preserved + new = still 2 dirs. Bounded.
expect(dirsPerDownload[2]).to.have.lengthOf(2,
`3rd download: ${dirsPerDownload[2].join(', ')}`);
expect(dirsPerDownload[2]).to.not.include(dirsPerDownload[0][0],
'The first (now orphaned) update directory must be pruned on the third check');
const secondDir = dirsPerDownload[1].find(d => d !== dirsPerDownload[0][0]);
expect(dirsPerDownload[2]).to.include(secondDir,
'The second (currently staged) update directory must be preserved on the third check');
});
});
});
});
// Regression test for https://github.com/electron/electron/issues/50200
//
// When checkForUpdates() is called again after an update has been staged,
// Squirrel creates a new temporary directory and prunes old ones. If the
// prune removes the directory that ShipItState.plist references while the
// second download is still in flight, a subsequent quitAndInstall() will
// fail with ENOENT and the app will never relaunch.
it('should install the staged update when quitAndInstall is called while a second check is in flight', async () => {
await cleanSquirrelCache();
await withUpdatableApp({
nextVersion: '2.0.0',
startFixture: 'update-race',
endFixture: 'update-race'
}, async (appPath, updateZipPath) => {
let downloadCount = 0;
let stalledResponse: express.Response | null = null;
server.get('/update-file', (req, res) => {
downloadCount++;
if (downloadCount === 1) {
// First download completes normally and stages the update.
res.download(updateZipPath);
} else {
// Second download: stall indefinitely to simulate a slow
// network. This keeps the second check "in progress" when
// quitAndInstall() fires. Hold onto the response so we can
// clean it up later.
stalledResponse = res;
}
});
server.get('/update-check', (req, res) => {
res.json({
url: `http://localhost:${port}/update-file`,
name: 'My Release Name',
notes: 'Theses are some release notes innit',
pub_date: (new Date()).toString()
});
});
const relaunchPromise = new Promise<void>((resolve) => {
server.get('/update-check/updated/:version', (req, res) => {
res.status(204).send();
resolve();
});
});
const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult).to.have.property('code', 0);
expect(launchResult.out).to.include('Update Downloaded');
expect(launchResult.out).to.include('Calling quitAndInstall mid-download');
// First check + first download + second check + stalled second download.
expect(requests).to.have.lengthOf(4);
expect(requests[0]).to.have.property('url', '/update-check');
expect(requests[1]).to.have.property('url', '/update-file');
expect(requests[2]).to.have.property('url', '/update-check');
expect(requests[3]).to.have.property('url', '/update-file');
// The second download must have been in flight (never completed)
// when quitAndInstall was called.
expect(launchResult.out).to.not.include('Unexpected second download completion');
});
// Unblock the stalled response now that the initial app has exited
// so the express server can shut down cleanly.
if (stalledResponse) {
(stalledResponse as express.Response).status(500).end();
}
// The originally staged update (2.0.0) must have been applied and
// the app must relaunch, proving the staged update directory was
// not pruned out from under ShipItState.plist.
await relaunchPromise;
expect(requests).to.have.lengthOf(5);
expect(requests[4].url).to.equal('/update-check/updated/2.0.0');
expect(requests[4].header('user-agent')).to.include('Electron/');
});
});
it('should update to lower version numbers', async () => {
await withUpdatableApp({
nextVersion: '0.0.1',

View File

@@ -0,0 +1,82 @@
const { app, autoUpdater } = require('electron');
const fs = require('node:fs');
const path = require('node:path');
process.on('uncaughtException', (err) => {
console.error(err);
process.exit(1);
});
let installInvoked = false;
autoUpdater.on('error', (err) => {
// Once quitAndInstall() has been invoked the second in-flight check may
// surface a cancellation/network error as the process tears down; ignore
// errors after that point so we test the actual install race, not teardown.
if (installInvoked) {
console.log('Ignoring post-install error:', err && err.message);
return;
}
console.error(err);
process.exit(1);
});
const urlPath = path.resolve(__dirname, '../../../../url.txt');
let feedUrl = process.argv[1];
if (feedUrl === 'remain-open') {
// Hold the event loop
setInterval(() => {});
} else {
if (!feedUrl || !feedUrl.startsWith('http')) {
feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}`;
} else {
fs.writeFileSync(urlPath, `${feedUrl}/updated`);
}
autoUpdater.setFeedURL({
url: feedUrl
});
autoUpdater.checkForUpdates();
autoUpdater.on('update-available', () => {
console.log('Update Available');
});
let downloadedOnce = false;
autoUpdater.on('update-downloaded', () => {
console.log('Update Downloaded');
if (!downloadedOnce) {
downloadedOnce = true;
// Simulate a periodic update check firing after an update was already
// staged. The test server is expected to stall this second download so
// that it remains in flight while we call quitAndInstall().
// The short delay lets checkForUpdatesCommand's RACCommand executing
// state settle; calling immediately would hit the command's "disabled"
// guard since RACCommand disallows concurrent execution.
setTimeout(() => {
autoUpdater.checkForUpdates();
// Give Squirrel enough time to enter the second check (creating a new
// temporary directory, which with the regression prunes the directory
// that the staged update lives in) before invoking the install.
setTimeout(() => {
console.log('Calling quitAndInstall mid-download');
installInvoked = true;
autoUpdater.quitAndInstall();
}, 3000);
}, 1000);
} else {
// Should not reach here — the second download is stalled on purpose.
console.log('Unexpected second download completion');
autoUpdater.quitAndInstall();
}
});
autoUpdater.on('update-not-available', () => {
console.error('No update available');
process.exit(1);
});
}

View File

@@ -0,0 +1,5 @@
{
"name": "electron-test-update-race",
"version": "1.0.0",
"main": "./index.js"
}

View File

@@ -0,0 +1,57 @@
const { app, autoUpdater } = require('electron');
const fs = require('node:fs');
const path = require('node:path');
process.on('uncaughtException', (err) => {
console.error(err);
process.exit(1);
});
autoUpdater.on('error', (err) => {
console.error(err);
process.exit(1);
});
const urlPath = path.resolve(__dirname, '../../../../url.txt');
let feedUrl = process.argv[1];
if (feedUrl === 'remain-open') {
// Hold the event loop
setInterval(() => {});
} else {
if (!feedUrl || !feedUrl.startsWith('http')) {
feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}`;
} else {
fs.writeFileSync(urlPath, `${feedUrl}/updated`);
}
autoUpdater.setFeedURL({
url: feedUrl
});
autoUpdater.checkForUpdates();
autoUpdater.on('update-available', () => {
console.log('Update Available');
});
let updateStackCount = 0;
autoUpdater.on('update-downloaded', () => {
updateStackCount++;
console.log('Update Downloaded');
if (updateStackCount > 2) {
autoUpdater.quitAndInstall();
} else {
setTimeout(() => {
autoUpdater.checkForUpdates();
}, 1000);
}
});
autoUpdater.on('update-not-available', () => {
console.error('No update available');
process.exit(1);
});
}

View File

@@ -0,0 +1,5 @@
{
"name": "electron-test-update-triple-stack",
"version": "1.0.0",
"main": "./index.js"
}