55package content
66
77import (
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
400443var _ storage.DirectAccess = & remoteContentStorage {}
0 commit comments