//////////////////////////////////////////////////////////////////////////////// // Copyright © 2022 xx foundation // // // // Use of this source code is governed by a license that can be found in the // // LICENSE file. // //////////////////////////////////////////////////////////////////////////////// //go:build js && wasm package logging import ( "encoding/binary" "encoding/json" "io" "math" "time" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/xxdk-wasm/worker" ) // TODO: add ability to import worker so that multiple threads can send logs: https://stackoverflow.com/questions/8343781/how-to-do-worker-to-worker-communication // workerLogger manages the recording of jwalterweatherman logs to the in-memory // file buffer in a remote Worker thread. type workerLogger struct { threshold jww.Threshold maxLogFileSize int wm *worker.Manager } // newWorkerLogger starts logging to an in-memory log file in a remote Worker // at the specified threshold. Returns a [workerLogger] that can be used to get // the log file. func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int, wasmJsPath, workerName string) (*workerLogger, error) { // Create new worker manager, which will start the worker and wait until // communication has been established wm, err := worker.NewManager(wasmJsPath, workerName, false) if err != nil { return nil, err } wl := &workerLogger{ threshold: threshold, maxLogFileSize: maxLogFileSize, wm: wm, } // Register the callback used by the Javascript to request the log file. // This prevents an error print when GetFileExtTag is not registered. wl.wm.RegisterCallback(GetFileExtTag, func([]byte) { jww.DEBUG.Print("[LOG] Received file requested from external " + "Javascript. Ignoring file.") }) data, err := json.Marshal(wl.maxLogFileSize) if err != nil { return nil, err } // Send message to initialize the log file listener errChan := make(chan error) wl.wm.SendMessage(NewLogFileTag, data, func(data []byte) { if len(data) > 0 { errChan <- errors.New(string(data)) } else { errChan <- nil } }) // Wait for worker to respond select { case err = <-errChan: if err != nil { return nil, err } case <-time.After(worker.ResponseTimeout): return nil, errors.Errorf("timed out after %s waiting for new log "+ "file in worker to initialize", worker.ResponseTimeout) } jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level "+ "%s using web worker %s", wl.maxLogFileSize, wl.threshold, workerName) logger = wl return wl, nil } // Write adheres to the io.Writer interface and sends the log entries to the // worker to be added to the file buffer. Always returns the length of p and // nil. All errors are printed to the log. func (wl *workerLogger) Write(p []byte) (n int, err error) { wl.wm.SendMessage(WriteLogTag, p, nil) return len(p), nil } // Listen adheres to the [jwalterweatherman.LogListener] type and returns the // log writer when the threshold is within the set threshold limit. func (wl *workerLogger) Listen(threshold jww.Threshold) io.Writer { if threshold < wl.threshold { return nil } return wl } // StopLogging stops log message writes and terminates the worker. Once logging // is stopped, it cannot be resumed and the log file cannot be recovered. func (wl *workerLogger) StopLogging() { wl.threshold = math.MaxInt wl.wm.Stop() jww.DEBUG.Printf("[LOG] Terminated log worker.") } // GetFile returns the entire log file. func (wl *workerLogger) GetFile() []byte { fileChan := make(chan []byte) wl.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data }) select { case file := <-fileChan: return file case <-time.After(worker.ResponseTimeout): jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ "file from worker", worker.ResponseTimeout) return nil } } // Threshold returns the log level threshold used in the file. func (wl *workerLogger) Threshold() jww.Threshold { return wl.threshold } // MaxSize returns the max size, in bytes, that the log file is allowed to be. func (wl *workerLogger) MaxSize() int { return wl.maxLogFileSize } // Size returns the number of bytes written to the log file. func (wl *workerLogger) Size() int { sizeChan := make(chan []byte) wl.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data }) select { case data := <-sizeChan: return int(binary.LittleEndian.Uint64(data)) case <-time.After(worker.ResponseTimeout): jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ "file size from worker", worker.ResponseTimeout) return 0 } } // Worker returns the manager for the Javascript Worker object. func (wl *workerLogger) Worker() *worker.Manager { return wl.wm }