mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
refactor(tracker): new closing mechanism logic
This commit is contained in:
parent
9f89c37956
commit
554a840982
@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
@ -71,7 +72,8 @@ func main() {
|
|||||||
go func() {
|
go func() {
|
||||||
trackerUi.Main()
|
trackerUi.Main()
|
||||||
audioCloser.Close()
|
audioCloser.Close()
|
||||||
detector.Close()
|
tracker.TrySend(broker.CloseDetector, struct{}{})
|
||||||
|
tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second)
|
||||||
if *cpuprofile != "" {
|
if *cpuprofile != "" {
|
||||||
pprof.StopCPUProfile()
|
pprof.StopCPUProfile()
|
||||||
f.Close()
|
f.Close()
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
"github.com/vsariola/sointu/cmd"
|
"github.com/vsariola/sointu/cmd"
|
||||||
@ -134,17 +135,22 @@ func init() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
CloseFunc: func() {
|
CloseFunc: func() {
|
||||||
broker.ToModel <- tracker.MsgToModel{Data: func() { t.ForceQuit().Do() }}
|
tracker.TrySend(broker.CloseDetector, struct{}{})
|
||||||
t.WaitQuitted()
|
tracker.TrySend(broker.CloseGUI, struct{}{})
|
||||||
detector.Close()
|
tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second)
|
||||||
|
tracker.TimeoutReceive(broker.FinishedGUI, 3*time.Second)
|
||||||
},
|
},
|
||||||
GetChunkFunc: func(isPreset bool) []byte {
|
GetChunkFunc: func(isPreset bool) []byte {
|
||||||
retChn := make(chan []byte)
|
retChn := make(chan []byte)
|
||||||
broker.ToModel <- tracker.MsgToModel{Data: func() { retChn <- t.MarshalRecovery() }}
|
|
||||||
return <-retChn
|
if !tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { retChn <- t.MarshalRecovery() }}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret, _ := tracker.TimeoutReceive(retChn, 5*time.Second) // ret will be nil if timeout or channel closed
|
||||||
|
return ret
|
||||||
},
|
},
|
||||||
SetChunkFunc: func(data []byte, isPreset bool) {
|
SetChunkFunc: func(data []byte, isPreset bool) {
|
||||||
broker.ToModel <- tracker.MsgToModel{Data: func() { t.UnmarshalRecovery(data) }}
|
tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { t.UnmarshalRecovery(data) }})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,10 +436,12 @@ func (m *Model) OpenSong() Action {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Quit() Action {
|
func (m *Model) RequestQuit() Action {
|
||||||
return Allow(func() {
|
return Allow(func() {
|
||||||
|
if !m.quitted {
|
||||||
m.dialog = QuitChanges
|
m.dialog = QuitChanges
|
||||||
m.completeAction(true)
|
m.completeAction(true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package tracker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
"github.com/vsariola/sointu/vm"
|
"github.com/vsariola/sointu/vm"
|
||||||
@ -16,11 +17,33 @@ type (
|
|||||||
// return buffers to pass buffers around without allocating new memory every
|
// return buffers to pass buffers around without allocating new memory every
|
||||||
// time. We can later consider making many-to-many types of communication
|
// time. We can later consider making many-to-many types of communication
|
||||||
// and more complex routing logic to the Broker if needed.
|
// and more complex routing logic to the Broker if needed.
|
||||||
|
//
|
||||||
|
// For closing goroutines, the broker has two channels for each goroutine:
|
||||||
|
// CloseXXX and FinishedXXX. The CloseXXX channel has a capacity of 1, so
|
||||||
|
// you can always send a empty message (struct{}{}) to it without blocking.
|
||||||
|
// If the channel is already full, that means someone else has already
|
||||||
|
// requested its closure and the goroutine is already closing, so dropping
|
||||||
|
// the message is fine. Then, FinishedXXX is used to signal that a goroutine
|
||||||
|
// has succesfully closed and cleaned up. Nothing is ever sent to the
|
||||||
|
// channel, it is only closed. You can wait until the goroutines is done
|
||||||
|
// closing with "<- FinishedXXX", which for avoiding deadlocks can be
|
||||||
|
// combined with a timeout:
|
||||||
|
// select {
|
||||||
|
// case <-FinishedXXX:
|
||||||
|
// case <-time.After(3 * time.Second):
|
||||||
|
// }
|
||||||
|
|
||||||
Broker struct {
|
Broker struct {
|
||||||
ToModel chan MsgToModel
|
ToModel chan MsgToModel
|
||||||
ToPlayer chan any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
|
ToPlayer chan any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
|
||||||
ToDetector chan MsgToDetector
|
ToDetector chan MsgToDetector
|
||||||
|
|
||||||
|
CloseDetector chan struct{}
|
||||||
|
CloseGUI chan struct{}
|
||||||
|
|
||||||
|
FinishedGUI chan struct{}
|
||||||
|
FinishedDetector chan struct{}
|
||||||
|
|
||||||
bufferPool sync.Pool
|
bufferPool sync.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +72,6 @@ type (
|
|||||||
// which gets executed in the detector goroutine.
|
// which gets executed in the detector goroutine.
|
||||||
MsgToDetector struct {
|
MsgToDetector struct {
|
||||||
Reset bool
|
Reset bool
|
||||||
Quit bool
|
|
||||||
Data any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
|
Data any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
|
||||||
|
|
||||||
WeightingType WeightingType
|
WeightingType WeightingType
|
||||||
@ -64,6 +86,10 @@ func NewBroker() *Broker {
|
|||||||
ToPlayer: make(chan interface{}, 1024),
|
ToPlayer: make(chan interface{}, 1024),
|
||||||
ToModel: make(chan MsgToModel, 1024),
|
ToModel: make(chan MsgToModel, 1024),
|
||||||
ToDetector: make(chan MsgToDetector, 1024),
|
ToDetector: make(chan MsgToDetector, 1024),
|
||||||
|
CloseDetector: make(chan struct{}, 1),
|
||||||
|
CloseGUI: make(chan struct{}, 1),
|
||||||
|
FinishedGUI: make(chan struct{}),
|
||||||
|
FinishedDetector: make(chan struct{}),
|
||||||
bufferPool: sync.Pool{New: func() interface{} { return &sointu.AudioBuffer{} }},
|
bufferPool: sync.Pool{New: func() interface{} { return &sointu.AudioBuffer{} }},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,3 +122,15 @@ func TrySend[T any](c chan<- T, v T) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TimeoutReceive is a helper function to block until a value is received from a
|
||||||
|
// channel, or timing out after t. ok will be false if the timeout occurred or
|
||||||
|
// if the channel is closed.
|
||||||
|
func TimeoutReceive[T any](c <-chan T, t time.Duration) (v T, ok bool) {
|
||||||
|
select {
|
||||||
|
case v, ok = <-c:
|
||||||
|
return v, ok
|
||||||
|
case <-time.After(t):
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -12,6 +12,7 @@ type (
|
|||||||
broker *Broker
|
broker *Broker
|
||||||
loudnessDetector loudnessDetector
|
loudnessDetector loudnessDetector
|
||||||
peakDetector peakDetector
|
peakDetector peakDetector
|
||||||
|
chunkHistory sointu.AudioBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
WeightingType int
|
WeightingType int
|
||||||
@ -98,15 +99,22 @@ func NewDetector(b *Broker) *Detector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Detector) Run() {
|
func (s *Detector) Run() {
|
||||||
var chunkHistory sointu.AudioBuffer
|
for {
|
||||||
for msg := range s.broker.ToDetector {
|
select {
|
||||||
|
case <-s.broker.CloseDetector:
|
||||||
|
close(s.broker.FinishedDetector)
|
||||||
|
return
|
||||||
|
case msg := <-s.broker.ToDetector:
|
||||||
|
s.handleMsg(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Detector) handleMsg(msg MsgToDetector) {
|
||||||
if msg.Reset {
|
if msg.Reset {
|
||||||
s.loudnessDetector.reset()
|
s.loudnessDetector.reset()
|
||||||
s.peakDetector.reset()
|
s.peakDetector.reset()
|
||||||
}
|
}
|
||||||
if msg.Quit {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if msg.HasWeightingType {
|
if msg.HasWeightingType {
|
||||||
s.loudnessDetector.weighting = weightings[WeightingType(msg.WeightingType)]
|
s.loudnessDetector.weighting = weightings[WeightingType(msg.WeightingType)]
|
||||||
s.loudnessDetector.reset()
|
s.loudnessDetector.reset()
|
||||||
@ -121,21 +129,21 @@ func (s *Detector) Run() {
|
|||||||
buf := *data
|
buf := *data
|
||||||
for {
|
for {
|
||||||
var chunk sointu.AudioBuffer
|
var chunk sointu.AudioBuffer
|
||||||
if len(chunkHistory) > 0 && len(chunkHistory) < 4410 {
|
if len(s.chunkHistory) > 0 && len(s.chunkHistory) < 4410 {
|
||||||
l := min(len(buf), 4410-len(chunkHistory))
|
l := min(len(buf), 4410-len(s.chunkHistory))
|
||||||
chunkHistory = append(chunkHistory, buf[:l]...)
|
s.chunkHistory = append(s.chunkHistory, buf[:l]...)
|
||||||
if len(chunkHistory) < 4410 {
|
if len(s.chunkHistory) < 4410 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
chunk = chunkHistory
|
chunk = s.chunkHistory
|
||||||
buf = buf[l:]
|
buf = buf[l:]
|
||||||
} else {
|
} else {
|
||||||
if len(buf) >= 4410 {
|
if len(buf) >= 4410 {
|
||||||
chunk = buf[:4410]
|
chunk = buf[:4410]
|
||||||
buf = buf[4410:]
|
buf = buf[4410:]
|
||||||
} else {
|
} else {
|
||||||
chunkHistory = chunkHistory[:0]
|
s.chunkHistory = s.chunkHistory[:0]
|
||||||
chunkHistory = append(chunkHistory, buf...)
|
s.chunkHistory = append(s.chunkHistory, buf...)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,16 +155,7 @@ func (s *Detector) Run() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
s.broker.PutAudioBuffer(data)
|
|
||||||
case func():
|
|
||||||
data()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close may theoretically block if the broker is full, but it should not happen in practice
|
|
||||||
func (s *Detector) Close() {
|
|
||||||
s.broker.ToDetector <- MsgToDetector{Quit: true}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeLoudnessDetector(weighting WeightingType) loudnessDetector {
|
func makeLoudnessDetector(weighting WeightingType) loudnessDetector {
|
||||||
|
@ -184,7 +184,7 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
|||||||
t.OpenSong().Do()
|
t.OpenSong().Do()
|
||||||
case "Quit":
|
case "Quit":
|
||||||
if canQuit {
|
if canQuit {
|
||||||
t.Quit().Do()
|
t.RequestQuit().Do()
|
||||||
}
|
}
|
||||||
case "SaveSong":
|
case "SaveSong":
|
||||||
t.SaveSong().Do()
|
t.SaveSong().Do()
|
||||||
|
@ -336,7 +336,7 @@ func NewMenuBar(model *tracker.Model) *MenuBar {
|
|||||||
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav...", ShortcutText: keyActionMap["ExportWav"], Doer: model.Export()},
|
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav...", ShortcutText: keyActionMap["ExportWav"], Doer: model.Export()},
|
||||||
}
|
}
|
||||||
if canQuit {
|
if canQuit {
|
||||||
ret.fileMenuItems = append(ret.fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit", ShortcutText: keyActionMap["Quit"], Doer: model.Quit()})
|
ret.fileMenuItems = append(ret.fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit", ShortcutText: keyActionMap["Quit"], Doer: model.RequestQuit()})
|
||||||
}
|
}
|
||||||
ret.editMenuItems = []MenuItem{
|
ret.editMenuItems = []MenuItem{
|
||||||
{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: keyActionMap["Undo"], Doer: model.Undo()},
|
{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: keyActionMap["Undo"], Doer: model.Undo()},
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
@ -50,7 +49,6 @@ type (
|
|||||||
|
|
||||||
filePathString tracker.String
|
filePathString tracker.String
|
||||||
|
|
||||||
quitWG sync.WaitGroup
|
|
||||||
execChan chan func()
|
execChan chan func()
|
||||||
preferences Preferences
|
preferences Preferences
|
||||||
|
|
||||||
@ -102,85 +100,21 @@ func NewTracker(model *tracker.Model) *Tracker {
|
|||||||
t.Theme.Palette.Fg = primaryColor
|
t.Theme.Palette.Fg = primaryColor
|
||||||
t.Theme.Palette.ContrastFg = black
|
t.Theme.Palette.ContrastFg = black
|
||||||
t.TrackEditor.scrollTable.Focus()
|
t.TrackEditor.scrollTable.Focus()
|
||||||
t.quitWG.Add(1)
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) Main() {
|
func (t *Tracker) Main() {
|
||||||
titleFooter := ""
|
|
||||||
w := t.newWindow()
|
|
||||||
t.InstrumentEditor.Focus()
|
t.InstrumentEditor.Focus()
|
||||||
recoveryTicker := time.NewTicker(time.Second * 30)
|
recoveryTicker := time.NewTicker(time.Second * 30)
|
||||||
t.Explorer = explorer.NewExplorer(w)
|
|
||||||
// Make a channel to read window events from.
|
|
||||||
events := make(chan event.Event)
|
|
||||||
// Make a channel to signal the end of processing a window event.
|
|
||||||
acks := make(chan struct{})
|
|
||||||
go eventLoop(w, events, acks)
|
|
||||||
var ops op.Ops
|
var ops op.Ops
|
||||||
for {
|
titlePath := ""
|
||||||
select {
|
for !t.Quitted() {
|
||||||
case e := <-t.Broker().ToModel:
|
w := t.newWindow()
|
||||||
t.ProcessMsg(e)
|
w.Option(app.Title(titleFromPath(titlePath)))
|
||||||
w.Invalidate()
|
|
||||||
case e := <-events:
|
|
||||||
switch e := e.(type) {
|
|
||||||
case app.DestroyEvent:
|
|
||||||
acks <- struct{}{}
|
|
||||||
if canQuit {
|
|
||||||
t.Quit().Do()
|
|
||||||
}
|
|
||||||
if !t.Quitted() {
|
|
||||||
// TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog
|
|
||||||
w = t.newWindow()
|
|
||||||
t.Explorer = explorer.NewExplorer(w)
|
t.Explorer = explorer.NewExplorer(w)
|
||||||
go eventLoop(w, events, acks)
|
acks := make(chan struct{})
|
||||||
}
|
events := make(chan event.Event)
|
||||||
case app.FrameEvent:
|
go func() {
|
||||||
if titleFooter != t.filePathString.Value() {
|
|
||||||
titleFooter = t.filePathString.Value()
|
|
||||||
if titleFooter != "" {
|
|
||||||
w.Option(app.Title(fmt.Sprintf("Sointu Tracker - %v", titleFooter)))
|
|
||||||
} else {
|
|
||||||
w.Option(app.Title("Sointu Tracker"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gtx := app.NewContext(&ops, e)
|
|
||||||
if t.Playing().Value() && t.Follow().Value() {
|
|
||||||
t.TrackEditor.scrollTable.RowTitleList.CenterOn(t.PlaySongRow())
|
|
||||||
}
|
|
||||||
t.Layout(gtx, w)
|
|
||||||
e.Frame(gtx.Ops)
|
|
||||||
acks <- struct{}{}
|
|
||||||
default:
|
|
||||||
acks <- struct{}{}
|
|
||||||
}
|
|
||||||
case <-recoveryTicker.C:
|
|
||||||
t.SaveRecovery()
|
|
||||||
}
|
|
||||||
if t.Quitted() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recoveryTicker.Stop()
|
|
||||||
w.Perform(system.ActionClose)
|
|
||||||
t.SaveRecovery()
|
|
||||||
t.quitWG.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tracker) newWindow() *app.Window {
|
|
||||||
w := new(app.Window)
|
|
||||||
w.Option(app.Title("Sointu Tracker"))
|
|
||||||
w.Option(app.Size(t.preferences.WindowSize()))
|
|
||||||
if t.preferences.Window.Maximized {
|
|
||||||
w.Option(app.Maximized.Option())
|
|
||||||
}
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
func eventLoop(w *app.Window, events chan<- event.Event, acks <-chan struct{}) {
|
|
||||||
// Iterate window events, sending each to the old event loop and waiting for
|
|
||||||
// a signal that processing is complete before iterating again.
|
|
||||||
for {
|
for {
|
||||||
ev := w.Event()
|
ev := w.Event()
|
||||||
events <- ev
|
events <- ev
|
||||||
@ -189,10 +123,64 @@ func eventLoop(w *app.Window, events chan<- event.Event, acks <-chan struct{}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
F:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e := <-t.Broker().ToModel:
|
||||||
|
t.ProcessMsg(e)
|
||||||
|
w.Invalidate()
|
||||||
|
case <-t.Broker().CloseGUI:
|
||||||
|
t.ForceQuit().Do()
|
||||||
|
w.Perform(system.ActionClose)
|
||||||
|
case e := <-events:
|
||||||
|
switch e := e.(type) {
|
||||||
|
case app.DestroyEvent:
|
||||||
|
if canQuit {
|
||||||
|
t.RequestQuit().Do()
|
||||||
|
}
|
||||||
|
acks <- struct{}{}
|
||||||
|
break F // this window is done, we need to create a new one
|
||||||
|
case app.FrameEvent:
|
||||||
|
if titlePath != t.filePathString.Value() {
|
||||||
|
titlePath = t.filePathString.Value()
|
||||||
|
w.Option(app.Title(titleFromPath(titlePath)))
|
||||||
|
}
|
||||||
|
gtx := app.NewContext(&ops, e)
|
||||||
|
if t.Playing().Value() && t.Follow().Value() {
|
||||||
|
t.TrackEditor.scrollTable.RowTitleList.CenterOn(t.PlaySongRow())
|
||||||
|
}
|
||||||
|
t.Layout(gtx, w)
|
||||||
|
e.Frame(gtx.Ops)
|
||||||
|
if t.Quitted() {
|
||||||
|
w.Perform(system.ActionClose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acks <- struct{}{}
|
||||||
|
case <-recoveryTicker.C:
|
||||||
|
t.SaveRecovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recoveryTicker.Stop()
|
||||||
|
t.SaveRecovery()
|
||||||
|
close(t.Broker().FinishedGUI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) WaitQuitted() {
|
func (t *Tracker) newWindow() *app.Window {
|
||||||
t.quitWG.Wait()
|
w := new(app.Window)
|
||||||
|
w.Option(app.Size(t.preferences.WindowSize()))
|
||||||
|
if t.preferences.Window.Maximized {
|
||||||
|
w.Option(app.Maximized.Option())
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func titleFromPath(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return "Sointu Tracker"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Sointu Tracker - %s", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
|
func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
|
||||||
|
@ -309,6 +309,6 @@ func FuzzModel(f *testing.F) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
closeChan <- struct{}{}
|
closeChan <- struct{}{}
|
||||||
broker.ToDetector <- tracker.MsgToDetector{Quit: true}
|
broker.CloseDetector <- struct{}{}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user