Skip to content

Commit 7844e41

Browse files
committed
every night my claude code told me lots things to do, have to fix lah
1 parent 322d5a0 commit 7844e41

11 files changed

Lines changed: 168 additions & 130 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ Download pre-built binaries from [GitHub Releases](https://github.com/projecteru
4242

4343
```bash
4444
# Linux amd64
45-
curl -fsSL -o cocoon https://github.com/projecteru2/cocoon/releases/download/v0.1.1/cocoon_0.1.1_Linux_x86_64.tar.gz
46-
tar -xzf cocoon_0.1.1_Linux_x86_64.tar.gz
45+
curl -fsSL -o cocoon https://github.com/projecteru2/cocoon/releases/download/v0.1.3/cocoon_0.1.3_Linux_x86_64.tar.gz
46+
tar -xzf cocoon_0.1.3_Linux_x86_64.tar.gz
4747
install -m 0755 cocoon /usr/local/bin/
4848

4949
# Or use go install

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/gofrs/flock v0.13.0
1111
github.com/google/go-containerregistry v0.21.0
1212
github.com/google/uuid v1.6.0
13+
github.com/opencontainers/go-digest v1.0.0
1314
github.com/projecteru2/core v0.0.0-20241016125006-ff909eefe04c
1415
github.com/spf13/cobra v1.10.2
1516
github.com/spf13/viper v1.21.0
@@ -39,7 +40,6 @@ require (
3940
github.com/mattn/go-isatty v0.0.19 // indirect
4041
github.com/mitchellh/go-homedir v1.1.0 // indirect
4142
github.com/mitchellh/mapstructure v1.5.0 // indirect
42-
github.com/opencontainers/go-digest v1.0.0 // indirect
4343
github.com/opencontainers/image-spec v1.1.1 // indirect
4444
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
4545
github.com/pkg/errors v0.9.1 // indirect

images/cloudimg/pull.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func pull(ctx context.Context, conf *config.Config, store storage.Store[imageInd
102102
return fmt.Errorf("rename blob: %w", err)
103103
}
104104
if err := os.Chmod(blobPath, 0o444); err != nil { //nolint:gosec // G302: intentionally world-readable
105-
return fmt.Errorf("chmod blob: %w", err)
105+
log.WithFunc("cloudimg.pull").Warnf(ctx, "chmod blob %s: %v", blobPath, err)
106106
}
107107
}
108108

images/digest.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package images
22

3-
import "strings"
3+
import godigest "github.com/opencontainers/go-digest"
44

5-
// Digest represents a content-addressable digest in "algorithm:hex" format (e.g., "sha256:abcdef...").
5+
// Digest represents a content-addressable digest in "algorithm:hex" format
6+
// (e.g., "sha256:abcdef..."). Backed by opencontainers/go-digest.
67
type Digest string
78

89
// NewDigest creates a Digest from a raw hex string, prefixing "sha256:".
910
func NewDigest(hex string) Digest {
10-
return Digest("sha256:" + hex)
11+
return Digest(godigest.NewDigestFromEncoded(godigest.SHA256, hex))
1112
}
1213

1314
// Hex returns the hex portion of the digest, stripping the algorithm prefix.
1415
func (d Digest) Hex() string {
15-
return strings.TrimPrefix(string(d), "sha256:")
16+
return godigest.Digest(d).Encoded()
1617
}
1718

1819
// String returns the full digest string including the algorithm prefix.

images/oci/pull.go

Lines changed: 92 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -41,44 +41,73 @@ type pullLayerResult struct {
4141
func pull(ctx context.Context, conf *config.Config, store storage.Store[imageIndex], imageRef string, tracker progress.Tracker) error {
4242
logger := log.WithFunc("oci.pull")
4343

44-
ref, digestHex, workDir, results, err := fetchAndProcess(ctx, conf, store, imageRef, tracker)
44+
// Phase 1: network I/O — no lock held.
45+
ref, digestHex, layers, err := fetchImage(ctx, imageRef)
4546
if err != nil {
4647
return err
4748
}
48-
if results == nil {
49-
logger.Debugf(ctx, "Already up to date: %s (digest: sha256:%s)", ref, digestHex)
50-
return nil
51-
}
52-
// Clean up workDir after commit (not before).
53-
defer os.RemoveAll(workDir) //nolint:errcheck
5449

55-
// Commit artifacts and update index atomically under flock.
56-
// No digest-only short-circuit here: fetchAndProcess proceeds even when the
57-
// digest matches but local files are invalid, so commitAndRecord must always
58-
// run to move repaired artifacts into place. commitAndRecord itself is
59-
// idempotent (skips rename when src == dst).
60-
tracker.OnEvent(ociProgress.Event{Phase: ociProgress.PhaseCommit, Index: -1, Total: len(results)})
61-
manifestDigest := images.NewDigest(digestHex)
62-
if err := store.Update(ctx, func(idx *imageIndex) error {
63-
return commitAndRecord(conf, idx, ref, manifestDigest, results)
64-
}); err != nil {
65-
return fmt.Errorf("update image index: %w", err)
66-
}
50+
// Phase 2: lock → idempotency check → process layers → commit.
51+
// GC uses the same locker, so it will wait until we finish.
52+
return store.Update(ctx, func(idx *imageIndex) error {
53+
// Idempotency: check if already pulled with same manifest and all files intact.
54+
if isUpToDate(conf, idx, ref, digestHex) {
55+
logger.Debugf(ctx, "Already up to date: %s (digest: sha256:%s)", ref, digestHex)
56+
return nil
57+
}
6758

68-
tracker.OnEvent(ociProgress.Event{Phase: ociProgress.PhaseDone, Index: -1, Total: len(results)})
69-
logger.Infof(ctx, "Pulled: %s (digest: sha256:%s, layers: %d)", ref, digestHex, len(results))
70-
return nil
59+
// Collect known boot layer digests from ALL entries for cross-image self-heal.
60+
knownBootHexes := collectBootHexes(idx)
61+
62+
tracker.OnEvent(ociProgress.Event{Phase: ociProgress.PhasePull, Index: -1, Total: len(layers)})
63+
64+
workDir, mkErr := os.MkdirTemp(conf.OCITempDir(), "pull-*")
65+
if mkErr != nil {
66+
return fmt.Errorf("create work dir: %w", mkErr)
67+
}
68+
defer os.RemoveAll(workDir) //nolint:errcheck
69+
70+
// Process layers concurrently with bounded parallelism.
71+
results := make([]pullLayerResult, len(layers))
72+
g, gctx := errgroup.WithContext(ctx)
73+
limit := conf.PoolSize
74+
if limit <= 0 {
75+
limit = runtime.NumCPU()
76+
}
77+
g.SetLimit(limit)
78+
79+
totalLayers := len(layers)
80+
for i, layer := range layers {
81+
g.Go(func() error {
82+
return processLayer(gctx, conf, i, totalLayers, layer, workDir, knownBootHexes, tracker, &results[i])
83+
})
84+
}
85+
if waitErr := g.Wait(); waitErr != nil {
86+
return fmt.Errorf("process layers: %w", waitErr)
87+
}
88+
89+
healCachedBootFiles(ctx, conf, layers, results, workDir)
90+
91+
tracker.OnEvent(ociProgress.Event{Phase: ociProgress.PhaseCommit, Index: -1, Total: len(results)})
92+
manifestDigest := images.NewDigest(digestHex)
93+
if err := commitAndRecord(conf, idx, ref, manifestDigest, results); err != nil {
94+
return err
95+
}
96+
97+
tracker.OnEvent(ociProgress.Event{Phase: ociProgress.PhaseDone, Index: -1, Total: len(results)})
98+
logger.Infof(ctx, "Pulled: %s (digest: sha256:%s, layers: %d)", ref, digestHex, len(results))
99+
return nil
100+
})
71101
}
72102

73-
// fetchAndProcess downloads the image and processes all layers concurrently.
74-
// Returns nil results if the image is already up-to-date.
75-
// The caller owns workDir cleanup via the returned path (empty when already up-to-date).
76-
func fetchAndProcess(ctx context.Context, conf *config.Config, store storage.Store[imageIndex], imageRef string, tracker progress.Tracker) (ref, digestHex, workDir string, results []pullLayerResult, err error) {
103+
// fetchImage resolves the image reference, fetches the manifest, and returns
104+
// the layer descriptors. No lock is held — this is pure network I/O.
105+
func fetchImage(ctx context.Context, imageRef string) (ref, digestHex string, layers []v1.Layer, err error) {
77106
logger := log.WithFunc("oci.pull")
78107

79108
parsedRef, parseErr := name.ParseReference(imageRef)
80109
if parseErr != nil {
81-
return "", "", "", nil, fmt.Errorf("invalid image reference %q: %w", imageRef, parseErr)
110+
return "", "", nil, fmt.Errorf("invalid image reference %q: %w", imageRef, parseErr)
82111
}
83112
ref = parsedRef.String()
84113

@@ -95,99 +124,61 @@ func fetchAndProcess(ctx context.Context, conf *config.Config, store storage.Sto
95124
remote.WithPlatform(platform),
96125
)
97126
if fetchErr != nil {
98-
return "", "", "", nil, fmt.Errorf("fetch image %s: %w", ref, fetchErr)
127+
return "", "", nil, fmt.Errorf("fetch image %s: %w", ref, fetchErr)
99128
}
100129

101130
manifest, digestErr := img.Digest()
102131
if digestErr != nil {
103-
return "", "", "", nil, fmt.Errorf("get manifest digest: %w", digestErr)
132+
return "", "", nil, fmt.Errorf("get manifest digest: %w", digestErr)
104133
}
105134
digestHex = manifest.Hex
106135

107-
// Idempotency: check if already pulled with same manifest and all files intact.
108-
// Also collect known boot layer digests so processLayer can target self-heal
109-
// even when the boot directory has been entirely deleted.
110-
var alreadyPulled bool
111-
knownBootHexes := make(map[string]struct{})
112-
if withErr := store.With(ctx, func(idx *imageIndex) error {
113-
// Collect boot layer digests from ALL entries for cross-image self-heal.
114-
// This ensures processLayer can recover boot files even when the current
115-
// ref has no prior index record (e.g., first pull sharing cached layers).
116-
for _, e := range idx.Images {
117-
if e == nil {
118-
continue
119-
}
120-
if e.KernelLayer != "" {
121-
knownBootHexes[e.KernelLayer.Hex()] = struct{}{}
122-
}
123-
if e.InitrdLayer != "" {
124-
knownBootHexes[e.InitrdLayer.Hex()] = struct{}{}
125-
}
126-
}
127-
128-
// Idempotency check: same ref and manifest digest with all files intact.
129-
entry, ok := idx.Images[ref]
130-
if !ok || entry == nil || entry.ManifestDigest != images.NewDigest(digestHex) {
131-
return nil
132-
}
133-
if !utils.ValidFile(conf.KernelPath(entry.KernelLayer.Hex())) ||
134-
!utils.ValidFile(conf.InitrdPath(entry.InitrdLayer.Hex())) {
135-
return nil
136-
}
137-
for _, layer := range entry.Layers {
138-
if !utils.ValidFile(conf.BlobPath(layer.Digest.Hex())) {
139-
return nil
140-
}
141-
}
142-
alreadyPulled = true
143-
return nil
144-
}); withErr != nil {
145-
return "", "", "", nil, fmt.Errorf("read image index: %w", withErr)
146-
}
147-
if alreadyPulled {
148-
return ref, digestHex, "", nil, nil
149-
}
150-
151136
layers, layersErr := img.Layers()
152137
if layersErr != nil {
153-
return "", "", "", nil, fmt.Errorf("get layers: %w", layersErr)
138+
return "", "", nil, fmt.Errorf("get layers: %w", layersErr)
154139
}
155140
if len(layers) == 0 {
156-
return "", "", "", nil, fmt.Errorf("image %s has no layers", ref)
141+
return "", "", nil, fmt.Errorf("image %s has no layers", ref)
157142
}
158143

159-
tracker.OnEvent(ociProgress.Event{Phase: ociProgress.PhasePull, Index: -1, Total: len(layers)})
144+
return ref, digestHex, layers, nil
145+
}
160146

161-
// Create working directory under temp. Caller is responsible for cleanup.
162-
workDir, mkErr := os.MkdirTemp(conf.OCITempDir(), "pull-*")
163-
if mkErr != nil {
164-
return "", "", "", nil, fmt.Errorf("create work dir: %w", mkErr)
147+
// isUpToDate checks if the image is already pulled with the same manifest digest
148+
// and all files (blobs, kernel, initrd) are intact on disk.
149+
func isUpToDate(conf *config.Config, idx *imageIndex, ref, digestHex string) bool {
150+
entry, ok := idx.Images[ref]
151+
if !ok || entry == nil || entry.ManifestDigest != images.NewDigest(digestHex) {
152+
return false
165153
}
166-
167-
// Process layers concurrently with bounded parallelism.
168-
results = make([]pullLayerResult, len(layers))
169-
g, gctx := errgroup.WithContext(ctx)
170-
limit := conf.PoolSize
171-
if limit <= 0 {
172-
limit = runtime.NumCPU()
154+
if !utils.ValidFile(conf.KernelPath(entry.KernelLayer.Hex())) ||
155+
!utils.ValidFile(conf.InitrdPath(entry.InitrdLayer.Hex())) {
156+
return false
173157
}
174-
g.SetLimit(limit)
175-
176-
totalLayers := len(layers)
177-
for i, layer := range layers {
178-
g.Go(func() error {
179-
return processLayer(gctx, conf, i, totalLayers, layer, workDir, knownBootHexes, tracker, &results[i])
180-
})
158+
for _, layer := range entry.Layers {
159+
if !utils.ValidFile(conf.BlobPath(layer.Digest.Hex())) {
160+
return false
161+
}
181162
}
163+
return true
164+
}
182165

183-
if waitErr := g.Wait(); waitErr != nil {
184-
os.RemoveAll(workDir) //nolint:errcheck,gosec
185-
return "", "", "", nil, fmt.Errorf("process layers: %w", waitErr)
166+
// collectBootHexes gathers boot layer digests from ALL index entries
167+
// for cross-image self-heal during processLayer.
168+
func collectBootHexes(idx *imageIndex) map[string]struct{} {
169+
hexes := make(map[string]struct{})
170+
for _, e := range idx.Images {
171+
if e == nil {
172+
continue
173+
}
174+
if e.KernelLayer != "" {
175+
hexes[e.KernelLayer.Hex()] = struct{}{}
176+
}
177+
if e.InitrdLayer != "" {
178+
hexes[e.InitrdLayer.Hex()] = struct{}{}
179+
}
186180
}
187-
188-
healCachedBootFiles(ctx, conf, layers, results, workDir)
189-
190-
return ref, digestHex, workDir, results, nil
181+
return hexes
191182
}
192183

193184
// moveBootFile renames a boot artifact (kernel or initrd) to its shared path,

lock/flock/flock.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,12 @@ func (l *Lock) commitFlock(acquire func(*flock.Flock) (bool, error)) (bool, erro
8383
fl := flock.New(l.path)
8484
locked, err := acquire(fl)
8585
if err != nil {
86+
_ = fl.Close()
8687
<-l.ch
8788
return false, err
8889
}
8990
if !locked {
91+
_ = fl.Close()
9092
<-l.ch
9193
return false, nil
9294
}

os-image/ubuntu/24.04-xface/Dockerfile

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# Use the latest Ubuntu 24.04 (Noble) LTS
21
FROM ubuntu:24.04
32

43
ARG TARGETARCH
@@ -20,14 +19,14 @@ RUN --mount=type=secret,id=cocoon_overlay \
2019
wget "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" && \
2120
apt-get install -y "./google-chrome-stable_current_amd64.deb" && \
2221
rm "google-chrome-stable_current_amd64.deb" && \
23-
sed -i 's/exec -a "$0" "$HERE\/chrome" "$@"/exec -a "$0" "$HERE\/chrome" "$@" --no-sandbox --user-data-dir/ ' /opt/google/chrome/google-chrome && \
22+
sed -i 's/exec -a "$0" "$HERE\/chrome" "$@"/exec -a "$0" "$HERE\/chrome" "$@" --no-sandbox --user-data-dir --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage/ ' /opt/google/chrome/google-chrome && \
2423
mkdir -p /etc/xdg/autostart && \
2524
cp /usr/share/applications/google-chrome.desktop /etc/xdg/autostart/; \
2625
else \
2726
add-apt-repository -y ppa:xtradeb/apps && \
2827
apt-get update && \
2928
apt-get install -y chromium && \
30-
sed -i 's/^Exec=chromium.*/Exec=chromium --no-sandbox --user-data-dir %U/' /usr/share/applications/chromium.desktop && \
29+
sed -i 's/^Exec=chromium.*/Exec=chromium --no-sandbox --user-data-dir --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage %U/' /usr/share/applications/chromium.desktop && \
3130
mkdir -p /etc/xdg/autostart && \
3231
cp /usr/share/applications/chromium.desktop /etc/xdg/autostart/; \
3332
fi && \
@@ -46,10 +45,15 @@ RUN --mount=type=secret,id=cocoon_overlay \
4645
printf "[Match]\nName=e* v*\n[Network]\nDHCP=yes\n" > /etc/systemd/network/20-wired.network && \
4746
echo "xfce4-session" > /root/.xsession && \
4847
sed -i 's/test -x \/etc\/X11\/Xsession && exec \/etc\/X11\/Xsession/exec \/usr\/bin\/startxfce4/g' /etc/xrdp/startwm.sh && \
48+
sed -i 's/^max_bpp=.*/max_bpp=16/' /etc/xrdp/xrdp.ini && \
49+
sed -i 's/^crypt_level=.*/crypt_level=none/' /etc/xrdp/xrdp.ini && \
50+
sed -i 's/^tcp_nodelay=.*/tcp_nodelay=true/' /etc/xrdp/xrdp.ini && \
51+
mkdir -p /root/.config/xfce4/xfconf/xfce-perchannel-xml && \
52+
printf '<?xml version="1.0" encoding="UTF-8"?>\n<channel name="xfwm4" version="1.0">\n <property name="general" type="empty">\n <property name="use_compositing" type="bool" value="false"/>\n </property>\n</channel>\n' > /root/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml && \
4953
adduser xrdp ssl-cert && \
5054
systemctl enable xrdp && \
5155
echo 'root:cocoon' | chpasswd && \
5256
rm -rf /var/lib/apt/lists/* && \
5357
mkdir -p /var/lib/apt/lists/partial /var/cache/apt/archives/partial
5458

55-
CMD ["/sbin/init"]
59+
CMD ["/sbin/init"]

utils/poll.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import (
99
// WaitFor polls check at the given interval until it returns (true, nil),
1010
// returns a non-nil error, or the timeout/context expires.
1111
func WaitFor(ctx context.Context, timeout, interval time.Duration, check func() (done bool, err error)) error {
12-
deadline := time.Now().Add(timeout)
12+
ctx, cancel := context.WithTimeout(ctx, timeout)
13+
defer cancel()
14+
15+
ticker := time.NewTicker(interval)
16+
defer ticker.Stop()
17+
1318
for {
14-
if time.Now().After(deadline) {
15-
return fmt.Errorf("timeout after %s", timeout)
16-
}
1719
done, err := check()
1820
if err != nil {
1921
return err
@@ -23,8 +25,11 @@ func WaitFor(ctx context.Context, timeout, interval time.Duration, check func()
2325
}
2426
select {
2527
case <-ctx.Done():
28+
if ctx.Err() == context.DeadlineExceeded {
29+
return fmt.Errorf("timeout after %s", timeout)
30+
}
2631
return ctx.Err()
27-
case <-time.After(interval):
32+
case <-ticker.C:
2833
}
2934
}
3035
}

0 commit comments

Comments
 (0)