Skip to content

Commit 1e87d69

Browse files
committed
WIP
Tool: gitpod/catfood.gitpod.cloud
1 parent 6e9020e commit 1e87d69

File tree

4 files changed

+104
-60
lines changed

4 files changed

+104
-60
lines changed

components/ws-daemon/cmd/content-initializer.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
package cmd
66

77
import (
8-
"fmt"
8+
"os"
99

1010
"github.com/spf13/cobra"
1111

@@ -18,13 +18,14 @@ var contentInitializerCmd = &cobra.Command{
1818
Short: "fork'ed by ws-daemon to initialize content",
1919
Args: cobra.ExactArgs(0),
2020
RunE: func(cmd *cobra.Command, args []string) error {
21-
stats, err := content.RunInitializerChild()
21+
22+
statsFd := os.NewFile(content.RUN_INITIALIZER_CHILD_STATS_FD, "stats")
23+
24+
err := content.RunInitializerChild(statsFd)
2225
if err != nil {
2326
return err
2427
}
2528

26-
fmt.Printf(content.FormatStatsBytes(stats))
27-
2829
return nil
2930
},
3031
}

components/ws-daemon/cmd/content-initializer/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ func main() {
2121
log.Init("content-initializer", "", true, false)
2222
tracing.Init("content-initializer")
2323

24-
stats, err := content.RunInitializerChild()
24+
statsFd := os.NewFile(content.RUN_INITIALIZER_CHILD_STATS_FD, "stats")
25+
26+
err := content.RunInitializerChild(statsFd)
2527
if err != nil {
26-
errfd := os.NewFile(uintptr(3), "errout")
28+
errfd := os.NewFile(content.RUN_INITIALIZER_CHILD_ERROUT_FD, "errout")
2729
_, _ = fmt.Fprintf(errfd, err.Error())
2830

2931
os.Exit(content.FAIL_CONTENT_INITIALIZER_EXIT_CODE)
3032
}
31-
32-
fmt.Printf(content.FormatStatsBytes(stats))
3333
}

components/ws-daemon/debug.Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RUN go get -u github.com/go-delve/delve/cmd/dlv
99
FROM cgr.dev/chainguard/wolfi-base:latest@sha256:a7db49b55bd97c12cd686272325bbac236830111db336e084b89f5c816ab0537 as dl
1010
WORKDIR /dl
1111
RUN apk add --no-cache curl file \
12-
&& curl -OsSL https://github.com/opencontainers/runc/releases/download/v1.1.12/runc.amd64 \
12+
&& curl -OsSL https://github.com/opencontainers/runc/releases/download/v1.1.14/runc.amd64 \
1313
&& chmod +x runc.amd64 \
1414
&& if ! file runc.amd64 | grep -iq "ELF 64-bit LSB pie executable"; then echo "runc.amd64 is not a binary file"; exit 1;fi
1515

components/ws-daemon/pkg/content/initializer.go

Lines changed: 94 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@
55
package content
66

77
import (
8-
"bufio"
98
"bytes"
109
"context"
1110
"encoding/json"
1211
"errors"
13-
"fmt"
14-
"io/ioutil"
12+
"io"
1513
"os"
1614
"os/exec"
1715
"path/filepath"
16+
"strconv"
1817
"strings"
1918
"syscall"
2019
"time"
@@ -253,37 +252,36 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
253252
name = "init-ws-" + opts.OWI.InstanceID
254253
}
255254

256-
args = append(args, "--log-format", "json", "run")
257-
args = append(args, "--preserve-fds", "1")
258-
args = append(args, name)
259-
255+
// pass a pipe "file" to the content init process as fd 3 to capture the error output
260256
errIn, errOut, err := os.Pipe()
261257
if err != nil {
262258
return nil, err
263259
}
264-
errch := make(chan []byte, 1)
265-
go func() {
266-
errmsg, _ := ioutil.ReadAll(errIn)
267-
errch <- errmsg
268-
}()
260+
defer errOut.Close()
261+
262+
// pass a pipe "file" to the content init process as fd 4 to capture the metrics output
263+
statsIn, statsOut, err := os.Pipe()
264+
if err != nil {
265+
return nil, err
266+
}
267+
defer statsOut.Close()
268+
269+
args = append(args, "--log-format", "json", "run")
270+
extraFiles := []*os.File{errOut, statsOut}
271+
args = append(args, "--preserve-fds", strconv.Itoa(len(extraFiles)))
272+
args = append(args, name)
269273

270274
var cmdOut bytes.Buffer
271275
cmd := exec.Command("runc", args...)
272276
cmd.Dir = tmpdir
273277
cmd.Stdout = &cmdOut
274278
cmd.Stderr = os.Stderr
275279
cmd.Stdin = os.Stdin
276-
cmd.ExtraFiles = []*os.File{errOut}
280+
cmd.ExtraFiles = extraFiles
277281
err = cmd.Run()
278282
log.FromBuffer(&cmdOut, log.WithFields(opts.OWI.Fields()))
279-
errOut.Close()
280283

281-
var errmsg []byte
282-
select {
283-
case errmsg = <-errch:
284-
case <-time.After(1 * time.Second):
285-
errmsg = []byte("failed to read content initializer response")
286-
}
284+
errmsg, statsBytes := waitForAndReadExtraFiles(errIn, statsIn)
287285
if err != nil {
288286
if exiterr, ok := err.(*exec.ExitError); ok {
289287
// The program has exited with an exit code != 0. If it's FAIL_CONTENT_INITIALIZER_EXIT_CODE, it was deliberate.
@@ -296,42 +294,76 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
296294
return nil, err
297295
}
298296

299-
stats := parseStats(&cmdOut)
300-
297+
stats := parseStats(statsBytes)
301298
return stats, nil
302299
}
303300

304-
func parseStats(buf *bytes.Buffer) *csapi.InitializerMetrics {
305-
scanner := bufio.NewScanner(buf)
306-
for scanner.Scan() {
307-
line := string(scanner.Bytes())
308-
if !strings.HasPrefix(line, STATS_PREFIX) {
309-
continue
301+
// waitForAndReadExtraFiles tries to read the content of the extra files passed to the content initializer, and waits up to 1s to do so
302+
func waitForAndReadExtraFiles(errIn *os.File, statsIn *os.File) (errmsg []byte, statsBytes []byte) {
303+
// read err
304+
errch := make(chan []byte, 1)
305+
go func() {
306+
errmsg, _ := io.ReadAll(errIn)
307+
errch <- errmsg
308+
}()
309+
310+
// read stats
311+
statsCh := make(chan []byte, 1)
312+
go func() {
313+
statsBytes, readErr := io.ReadAll(statsIn)
314+
if readErr != nil {
315+
log.WithError(readErr).Error("cannot read stats")
310316
}
317+
log.WithField("statsBytes", log.TrustedValueWrap{Value: string(statsBytes)}).Info("read stats")
318+
statsCh <- statsBytes
319+
}()
311320

312-
b := strings.TrimSpace(strings.TrimPrefix(line, STATS_PREFIX))
313-
var stats csapi.InitializerMetrics
314-
err := json.Unmarshal([]byte(b), &stats)
315-
if err != nil {
316-
log.WithError(err).WithField("line", line).Error("cannot unmarshal stats")
317-
return nil
321+
readFiles := 0
322+
for {
323+
select {
324+
case errmsg = <-errch:
325+
readFiles += 1
326+
case statsBytes = <-statsCh:
327+
readFiles += 1
328+
case <-time.After(1 * time.Second):
329+
if errmsg == nil {
330+
errmsg = []byte("failed to read content initializer response")
331+
}
332+
return
333+
}
334+
if readFiles == 2 {
335+
return
318336
}
319-
return &stats
320337
}
321-
return nil
322338
}
323339

340+
func parseStats(statsBytes []byte) *csapi.InitializerMetrics {
341+
var stats csapi.InitializerMetrics
342+
err := json.Unmarshal(statsBytes, &stats)
343+
if err != nil {
344+
log.WithError(err).WithField("bytes", log.TrustedValueWrap{Value: statsBytes}).Error("cannot unmarshal stats")
345+
return nil
346+
}
347+
return &stats
348+
}
349+
350+
// RUN_INITIALIZER_CHILD_ERROUT_FD is the fileDescriptor of the "errout" file descriptor passed to the content initializer
351+
const RUN_INITIALIZER_CHILD_ERROUT_FD = 3
352+
353+
// RUN_INITIALIZER_CHILD_STATS_FD is the fileDescriptor of the "stats" file descriptor passed to the content initializer
354+
const RUN_INITIALIZER_CHILD_STATS_FD = 4
355+
324356
// RunInitializerChild is the function that's expected to run when we call `/proc/self/exe content-initializer`
325-
func RunInitializerChild() (serializedStats []byte, err error) {
357+
func RunInitializerChild(statsFd *os.File) (err error) {
326358
fc, err := os.ReadFile("/content.json")
327359
if err != nil {
328-
return nil, err
360+
return err
329361
}
330362

331363
var initmsg msgInitContent
332364
err = json.Unmarshal(fc, &initmsg)
333365
if err != nil {
334-
return nil, err
366+
return err
335367
}
336368
log.Log = logrus.WithFields(initmsg.OWI)
337369

@@ -348,15 +380,15 @@ func RunInitializerChild() (serializedStats []byte, err error) {
348380
var req csapi.WorkspaceInitializer
349381
err = proto.Unmarshal(initmsg.Initializer, &req)
350382
if err != nil {
351-
return nil, err
383+
return err
352384
}
353385

354386
rs := &remoteContentStorage{RemoteContent: initmsg.RemoteContent}
355387

356388
dst := initmsg.Destination
357389
initializer, err := wsinit.NewFromRequest(ctx, dst, rs, &req, wsinit.NewFromRequestOpts{ForceGitpodUserForGit: false})
358390
if err != nil {
359-
return nil, err
391+
return err
360392
}
361393

362394
initSource, stats, err := wsinit.InitializeWorkspace(ctx, dst, rs,
@@ -366,35 +398,46 @@ func RunInitializerChild() (serializedStats []byte, err error) {
366398
wsinit.WithCleanSlate,
367399
)
368400
if err != nil {
369-
return nil, err
401+
return err
370402
}
371403

372404
// some workspace content may have a `/dst/.gitpod` file or directory. That would break
373405
// the workspace ready file placement (see https://github.com/gitpod-io/gitpod/issues/7694).
374406
err = wsinit.EnsureCleanDotGitpodDirectory(ctx, dst)
375407
if err != nil {
376-
return nil, err
408+
return err
377409
}
378410

379411
// Place the ready file to make Theia "open its gates"
380412
err = wsinit.PlaceWorkspaceReadyFile(ctx, dst, initSource, stats, initmsg.UID, initmsg.GID)
381413
if err != nil {
382-
return nil, err
414+
return err
383415
}
384416

385417
// Serialize metrics, so we can pass them back to the caller
386-
serializedStats, err = json.Marshal(stats)
387-
if err != nil {
388-
return nil, err
418+
if statsFd != nil {
419+
defer statsFd.Close()
420+
writeStats(statsFd, &stats)
421+
} else {
422+
log.Warn("no stats file descriptor provided")
389423
}
390424

391-
return serializedStats, nil
425+
return nil
392426
}
393427

394-
const STATS_PREFIX = "STATS:"
428+
func writeStats(statsFd *os.File, stats *csapi.InitializerMetrics) {
429+
serializedStats, err := json.Marshal(stats)
430+
if err != nil {
431+
log.WithError(err).Warn("cannot serialize stats")
432+
return
433+
}
395434

396-
func FormatStatsBytes(statsBytes []byte) string {
397-
return fmt.Sprintf("%s %s\n", STATS_PREFIX, string(statsBytes))
435+
log.WithField("stats", log.TrustedValueWrap{Value: string(serializedStats)}).Info("writing stats to fd")
436+
_, writeErr := statsFd.Write(serializedStats)
437+
if writeErr != nil {
438+
log.WithError(writeErr).Warn("error writing stats to fd")
439+
return
440+
}
398441
}
399442

400443
var _ storage.DirectAccess = &remoteContentStorage{}

0 commit comments

Comments
 (0)