feat: add preferred debug port planning
This commit is contained in:
+226
-37
@@ -205,27 +205,30 @@ type proxyPickerState struct {
|
||||
}
|
||||
|
||||
// InstanceRow is the Gio view model for one configured browser instance.
|
||||
// Runtime fields are refreshed independently and are never saved by the edit form.
|
||||
// PreferredRemoteDebugPort is saved configuration; runtime fields are refreshed
|
||||
// independently and are never saved by the edit form.
|
||||
type InstanceRow struct {
|
||||
ID string
|
||||
Name string
|
||||
Browser string
|
||||
UserDataDir string
|
||||
TargetURL string
|
||||
ProxyID string
|
||||
PID int
|
||||
RemoteDebugPort int
|
||||
OccupancySource string
|
||||
Status string
|
||||
ID string
|
||||
Name string
|
||||
Browser string
|
||||
UserDataDir string
|
||||
TargetURL string
|
||||
ProxyID string
|
||||
PreferredRemoteDebugPort int
|
||||
PID int
|
||||
RemoteDebugPort int
|
||||
OccupancySource string
|
||||
Status string
|
||||
}
|
||||
|
||||
type SettingsState struct {
|
||||
ChromePath string
|
||||
EdgePath string
|
||||
DefaultDir string
|
||||
LogDir string
|
||||
RemoteDebugStartPort int
|
||||
CloseOnExit bool
|
||||
ChromePath string
|
||||
EdgePath string
|
||||
DefaultDir string
|
||||
LogDir string
|
||||
RemoteDebugStartPort int
|
||||
ReservedRemoteDebugPorts []int
|
||||
CloseOnExit bool
|
||||
}
|
||||
|
||||
// Shell owns every interactive Gio widget. Keeping this state outside Layout
|
||||
@@ -262,6 +265,7 @@ type Shell struct {
|
||||
instanceName widget.Editor
|
||||
instanceDir widget.Editor
|
||||
instanceURL widget.Editor
|
||||
instancePort widget.Editor
|
||||
createProxyPick widget.Clickable
|
||||
editProxyPick widget.Clickable
|
||||
settingsProxyPick widget.Clickable
|
||||
@@ -269,6 +273,7 @@ type Shell struct {
|
||||
editName widget.Editor
|
||||
editDir widget.Editor
|
||||
editURL widget.Editor
|
||||
editPreferredPort widget.Editor
|
||||
editPort widget.Editor
|
||||
editBrowserKind widget.Enum
|
||||
editDirPick widget.Clickable
|
||||
@@ -353,10 +358,10 @@ type Shell struct {
|
||||
|
||||
func NewShell(theme *material.Theme) *Shell {
|
||||
s := &Shell{theme: theme, rows: []InstanceRow{
|
||||
{ID: "demo-operations", Name: "运营主账号", Browser: "Chrome", UserDataDir: `C:\Users\Public\chub\profiles\operations`, PID: 18420, RemoteDebugPort: 9666, Status: "运行中"},
|
||||
{ID: "demo-ads", Name: "广告投放", Browser: "Edge", UserDataDir: `C:\Users\Public\chub\profiles\ads`, Status: "启动中"},
|
||||
{ID: "demo-assets", Name: "素材采集", Browser: "Chrome", UserDataDir: `C:\Users\Public\chub\profiles\assets`, PID: 16108, RemoteDebugPort: 9668, OccupancySource: "browser_message_window", Status: "外部已关联"},
|
||||
{ID: "demo-backup", Name: "备用环境", Browser: "Edge", UserDataDir: `C:\Users\Public\chub\profiles\backup`, Status: "已退出"},
|
||||
{ID: "demo-operations", Name: "运营主账号", Browser: "Chrome", UserDataDir: `C:\Users\Public\chub\profiles\operations`, PreferredRemoteDebugPort: 9666, PID: 18420, RemoteDebugPort: 9666, Status: "运行中"},
|
||||
{ID: "demo-ads", Name: "广告投放", Browser: "Edge", UserDataDir: `C:\Users\Public\chub\profiles\ads`, PreferredRemoteDebugPort: 9667, Status: "启动中"},
|
||||
{ID: "demo-assets", Name: "素材采集", Browser: "Chrome", UserDataDir: `C:\Users\Public\chub\profiles\assets`, PreferredRemoteDebugPort: 9668, PID: 16108, RemoteDebugPort: 9668, OccupancySource: "browser_message_window", Status: "外部已关联"},
|
||||
{ID: "demo-backup", Name: "备用环境", Browser: "Edge", UserDataDir: `C:\Users\Public\chub\profiles\backup`, PreferredRemoteDebugPort: 9669, Status: "已退出"},
|
||||
}, searches: map[PathField]*pathSearchState{
|
||||
PathChromeExecutable: {},
|
||||
PathEdgeExecutable: {},
|
||||
@@ -368,6 +373,7 @@ func NewShell(theme *material.Theme) *Shell {
|
||||
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
|
||||
s.logDir.SetText(`C:\Users\Public\chub\logs`)
|
||||
s.remoteDebugPort.SetText(strconv.Itoa(domain.DefaultRemoteDebugPort))
|
||||
s.instancePort.SetText(strconv.Itoa(s.recommendRemoteDebugPort("")))
|
||||
s.closeOnExit.Value = true
|
||||
s.browserKind.Value = "chrome"
|
||||
s.editBrowserKind.Value = "chrome"
|
||||
@@ -385,6 +391,7 @@ func (s *Shell) SetInstances(rows []InstanceRow) {
|
||||
s.rows[i].ID = fmt.Sprintf("restored-%d", s.nextInstance)
|
||||
}
|
||||
}
|
||||
s.assignPreferredRemoteDebugPorts()
|
||||
if s.selectedInstanceID == "" && len(s.rows) > 0 {
|
||||
s.selectedInstanceID = s.rows[0].ID
|
||||
}
|
||||
@@ -555,7 +562,7 @@ func (s *Shell) consumeControls(gtx layout.Context) {
|
||||
s.page = pageSettings
|
||||
}
|
||||
for s.newClick.Clicked(gtx) {
|
||||
s.page = pageCreate
|
||||
s.beginCreate()
|
||||
}
|
||||
for s.refreshClick.Clicked(gtx) {
|
||||
s.requestRefresh()
|
||||
@@ -591,6 +598,12 @@ func (s *Shell) consumeControls(gtx layout.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) beginCreate() {
|
||||
s.page = pageCreate
|
||||
s.formFeedback = ""
|
||||
s.instancePort.SetText(strconv.Itoa(s.recommendRemoteDebugPort("")))
|
||||
}
|
||||
|
||||
func (s *Shell) consumeKeyboard(gtx layout.Context) {
|
||||
for {
|
||||
event, ok := gtx.Event(
|
||||
@@ -684,7 +697,8 @@ func (s *Shell) beginEdit(id string) {
|
||||
s.editName.SetText(row.Name)
|
||||
s.editDir.SetText(row.UserDataDir)
|
||||
s.editURL.SetText(row.TargetURL)
|
||||
s.editPort.SetText(remoteDebugPortText(row.RemoteDebugPort))
|
||||
s.editPreferredPort.SetText(preferredRemoteDebugPortText(row.PreferredRemoteDebugPort))
|
||||
s.editPort.SetText(actualRemoteDebugPortText(row.RemoteDebugPort))
|
||||
s.editProxyID = row.ProxyID
|
||||
if strings.EqualFold(row.Browser, "Edge") {
|
||||
s.editBrowserKind.Value = "edge"
|
||||
@@ -715,7 +729,7 @@ func (s *Shell) editIsDirty() bool {
|
||||
if s.editLocked(s.editOriginal) {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(s.editDir.Text()) != s.editOriginal.UserDataDir || browserDisplay(s.editBrowserKind.Value) != s.editOriginal.Browser
|
||||
return strings.TrimSpace(s.editDir.Text()) != s.editOriginal.UserDataDir || browserDisplay(s.editBrowserKind.Value) != s.editOriginal.Browser || strings.TrimSpace(s.editPreferredPort.Text()) != preferredRemoteDebugPortText(s.editOriginal.PreferredRemoteDebugPort)
|
||||
}
|
||||
|
||||
func (s *Shell) cancelEdit() {
|
||||
@@ -752,8 +766,18 @@ func (s *Shell) saveEdit() {
|
||||
s.editFeedback = "User Data Dir 必须是绝对路径。"
|
||||
return
|
||||
}
|
||||
preferredPort, err := normalizePreferredRemoteDebugPort(s.editPreferredPort.Text())
|
||||
if err != nil {
|
||||
s.editFeedback = err.Error()
|
||||
return
|
||||
}
|
||||
if s.remoteDebugPortReservedByOther(preferredPort, row.ID) {
|
||||
s.editFeedback = fmt.Sprintf("端口 %d 已被其他 Chub 实例预留或使用。", preferredPort)
|
||||
return
|
||||
}
|
||||
row.UserDataDir = filepath.Clean(userDataDir)
|
||||
row.Browser = browserDisplay(s.editBrowserKind.Value)
|
||||
row.PreferredRemoteDebugPort = preferredPort
|
||||
}
|
||||
row.Name = name
|
||||
row.TargetURL = targetURL
|
||||
@@ -916,7 +940,7 @@ func (s *Shell) instanceListRow(row InstanceRow) layout.Widget {
|
||||
layout.Flexed(instanceNameColumnWeight, material.Body1(s.theme, row.Name).Layout),
|
||||
layout.Flexed(instanceBrowserColumnWeight, material.Body2(s.theme, row.Browser).Layout),
|
||||
layout.Flexed(instanceDirectoryColumnWeight, pathCell(s.theme, row.UserDataDir)),
|
||||
layout.Flexed(instancePortColumnWeight, remoteDebugPortCell(s.theme, row.RemoteDebugPort)),
|
||||
layout.Flexed(instancePortColumnWeight, remoteDebugPortCell(s.theme, row)),
|
||||
layout.Flexed(instanceStatusColumnWeight, statusLabel(s.theme, row.Status)),
|
||||
)
|
||||
})
|
||||
@@ -966,17 +990,47 @@ func pathCell(theme *material.Theme, path string) layout.Widget {
|
||||
return style.Layout
|
||||
}
|
||||
|
||||
func remoteDebugPortCell(theme *material.Theme, port int) layout.Widget {
|
||||
return material.Body2(theme, remoteDebugPortText(port)).Layout
|
||||
func remoteDebugPortCell(theme *material.Theme, row InstanceRow) layout.Widget {
|
||||
style := material.Body2(theme, instancePortLabel(row))
|
||||
style.MaxLines = 1
|
||||
return style.Layout
|
||||
}
|
||||
|
||||
func remoteDebugPortText(port int) string {
|
||||
func actualRemoteDebugPortText(port int) string {
|
||||
if port > 0 {
|
||||
return strconv.Itoa(port)
|
||||
}
|
||||
return "—"
|
||||
}
|
||||
|
||||
func preferredRemoteDebugPortText(port int) string {
|
||||
if domain.ValidRemoteDebugPort(port) {
|
||||
return strconv.Itoa(port)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func instancePortLabel(row InstanceRow) string {
|
||||
if row.Status == "外部已关联" {
|
||||
if domain.ValidRemoteDebugPort(row.RemoteDebugPort) {
|
||||
return fmt.Sprintf("外部 %d", row.RemoteDebugPort)
|
||||
}
|
||||
return "—"
|
||||
}
|
||||
if row.Status == "启动中" && domain.ValidRemoteDebugPort(row.PreferredRemoteDebugPort) {
|
||||
return fmt.Sprintf("准备 %d", row.PreferredRemoteDebugPort)
|
||||
}
|
||||
if domain.ValidRemoteDebugPort(row.RemoteDebugPort) {
|
||||
return fmt.Sprintf("实际 %d", row.RemoteDebugPort)
|
||||
}
|
||||
if row.Status == "已退出" || row.Status == "启动失败" {
|
||||
if domain.ValidRemoteDebugPort(row.PreferredRemoteDebugPort) {
|
||||
return fmt.Sprintf("建议 %d", row.PreferredRemoteDebugPort)
|
||||
}
|
||||
}
|
||||
return "—"
|
||||
}
|
||||
|
||||
func (s *Shell) instanceActionButtons(row InstanceRow) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.E.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -1080,9 +1134,11 @@ func (s *Shell) editDialogCard(row InstanceRow) layout.Widget {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.formField("启动 URL(可选)", "仅支持 http/https;留空时启动浏览器默认页", &s.editURL)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.editPreferredPortField(gtx, locked) }),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerEdit) }),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.formField("当前调试端口", "运行时由全局起始端口分配,此处只读", &s.editPort)),
|
||||
layout.Rigid(s.formField("实际调试端口", "仅在已验证的运行或外部关联状态显示;此处只读", &s.editPort)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(statusLabel(s.theme, row.Status)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
@@ -1142,6 +1198,16 @@ func (s *Shell) editDirectoryField(gtx layout.Context, locked bool) layout.Dimen
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) editPreferredPortField(gtx layout.Context, locked bool) layout.Dimensions {
|
||||
s.editPreferredPort.ReadOnly = locked
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, "首选调试端口").Layout),
|
||||
layout.Rigid(material.Caption(s.theme, "下次启动优先使用;启动时仍会在后台复检 loopback 可用性").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(material.Editor(s.theme, &s.editPreferredPort, "例如:9666").Layout),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) editFeedbackLabel(locked bool) layout.Widget {
|
||||
label := s.editFeedback
|
||||
if label == "" && locked {
|
||||
@@ -1667,6 +1733,8 @@ func (s *Shell) createInstance(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.formField("启动 URL(可选)", "仅支持 http/https;留空时启动浏览器默认页", &s.instanceURL)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
|
||||
layout.Rigid(s.formField("首选调试端口", "已按现有实例推荐;启动时会在后台复检 loopback 可用性", &s.instancePort)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerCreate) }),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
|
||||
layout.Rigid(material.Body2(s.theme, "浏览器类型").Layout),
|
||||
@@ -1893,15 +1961,25 @@ func (s *Shell) createInstanceFromForm() {
|
||||
s.formFeedback = "请选择或输入 User Data Dir 后再创建。"
|
||||
return
|
||||
}
|
||||
preferredPort, err := normalizePreferredRemoteDebugPort(s.instancePort.Text())
|
||||
if err != nil {
|
||||
s.formFeedback = err.Error()
|
||||
return
|
||||
}
|
||||
if s.remoteDebugPortReservedByOther(preferredPort, "") {
|
||||
s.formFeedback = fmt.Sprintf("端口 %d 已被其他 Chub 实例预留或使用。", preferredPort)
|
||||
return
|
||||
}
|
||||
s.nextInstance++
|
||||
s.rows = append(s.rows, InstanceRow{
|
||||
ID: fmt.Sprintf("instance-%d", s.nextInstance),
|
||||
Name: name,
|
||||
Browser: browserDisplay(s.browserKind.Value),
|
||||
UserDataDir: userDataDir,
|
||||
TargetURL: strings.TrimSpace(s.instanceURL.Text()),
|
||||
ProxyID: s.createProxyID,
|
||||
Status: "已退出",
|
||||
ID: fmt.Sprintf("instance-%d", s.nextInstance),
|
||||
Name: name,
|
||||
Browser: browserDisplay(s.browserKind.Value),
|
||||
UserDataDir: userDataDir,
|
||||
TargetURL: strings.TrimSpace(s.instanceURL.Text()),
|
||||
ProxyID: s.createProxyID,
|
||||
PreferredRemoteDebugPort: preferredPort,
|
||||
Status: "已退出",
|
||||
})
|
||||
s.page = pageInstances
|
||||
s.formFeedback = ""
|
||||
@@ -2247,6 +2325,7 @@ func (s *Shell) requestStart(id string) {
|
||||
s.instanceFeedback = err.Error()
|
||||
return
|
||||
}
|
||||
settings.ReservedRemoteDebugPorts = s.reservedRemoteDebugPorts(id)
|
||||
if state == nil {
|
||||
state = &instanceStartState{}
|
||||
s.startStates[id] = state
|
||||
@@ -2333,11 +2412,21 @@ func (s *Shell) consumeStartResults() {
|
||||
status = "运行中(调试不可用)"
|
||||
}
|
||||
s.setInstanceRuntime(result.id, status, result.outcome.PID, result.outcome.RemoteDebugPort, "chub_registry")
|
||||
fallbackPort := result.outcome.Warning == "" && domain.ValidRemoteDebugPort(result.outcome.RemoteDebugPort) && row.PreferredRemoteDebugPort != result.outcome.RemoteDebugPort
|
||||
if fallbackPort {
|
||||
if updated, exists := s.instanceRow(result.id); exists {
|
||||
updated.PreferredRemoteDebugPort = result.outcome.RemoteDebugPort
|
||||
s.replaceInstanceRow(updated)
|
||||
}
|
||||
s.notifyInstancesChanged()
|
||||
}
|
||||
if s.applyPendingManagedExit(result.id, result.request, result.outcome.PID) {
|
||||
continue
|
||||
}
|
||||
if result.outcome.Warning != "" {
|
||||
s.instanceFeedback = fmt.Sprintf("%s 已启动(PID %d),但%s。", row.Name, result.outcome.PID, result.outcome.Warning)
|
||||
} else if fallbackPort {
|
||||
s.instanceFeedback = fmt.Sprintf("%s 的首选端口已被占用,已改用并保存端口 %d。", row.Name, result.outcome.RemoteDebugPort)
|
||||
} else {
|
||||
s.instanceFeedback = fmt.Sprintf("已启动 %s(PID %d,端口 %d)。", row.Name, result.outcome.PID, result.outcome.RemoteDebugPort)
|
||||
}
|
||||
@@ -2482,7 +2571,7 @@ func (s *Shell) applyManagedExit(result ManagedInstanceExit) bool {
|
||||
}
|
||||
|
||||
func instanceRefreshFingerprint(row InstanceRow) string {
|
||||
return strings.Join([]string{row.ID, row.Name, row.Browser, row.UserDataDir, row.TargetURL, row.ProxyID}, "\x00")
|
||||
return strings.Join([]string{row.ID, row.Name, row.Browser, row.UserDataDir, row.TargetURL, row.ProxyID, strconv.Itoa(row.PreferredRemoteDebugPort)}, "\x00")
|
||||
}
|
||||
|
||||
func (s *Shell) requestDelete(id string) {
|
||||
@@ -2653,6 +2742,106 @@ func normalizeRemoteDebugStartPort(value string) (int, error) {
|
||||
return port, nil
|
||||
}
|
||||
|
||||
func normalizePreferredRemoteDebugPort(value string) (int, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, fmt.Errorf("请输入首选调试端口(%d 到 %d)。", domain.MinRemoteDebugPort, domain.MaxRemoteDebugPort)
|
||||
}
|
||||
port, err := strconv.Atoi(value)
|
||||
if err != nil || !domain.ValidRemoteDebugPort(port) {
|
||||
return 0, fmt.Errorf("首选调试端口必须在 %d 到 %d 之间。", domain.MinRemoteDebugPort, domain.MaxRemoteDebugPort)
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
func (s *Shell) assignPreferredRemoteDebugPorts() {
|
||||
used := make(map[int]struct{}, len(s.rows)*2)
|
||||
for _, row := range s.rows {
|
||||
if domain.ValidRemoteDebugPort(row.PreferredRemoteDebugPort) {
|
||||
used[row.PreferredRemoteDebugPort] = struct{}{}
|
||||
}
|
||||
if domain.ValidRemoteDebugPort(row.RemoteDebugPort) {
|
||||
used[row.RemoteDebugPort] = struct{}{}
|
||||
}
|
||||
}
|
||||
for index := range s.rows {
|
||||
if domain.ValidRemoteDebugPort(s.rows[index].PreferredRemoteDebugPort) {
|
||||
continue
|
||||
}
|
||||
port := s.nextUnreservedRemoteDebugPort(used)
|
||||
if port == 0 {
|
||||
continue
|
||||
}
|
||||
s.rows[index].PreferredRemoteDebugPort = port
|
||||
used[port] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) recommendRemoteDebugPort(excludeID string) int {
|
||||
used := make(map[int]struct{}, len(s.rows)*2)
|
||||
for _, row := range s.rows {
|
||||
if row.ID == excludeID {
|
||||
continue
|
||||
}
|
||||
if domain.ValidRemoteDebugPort(row.PreferredRemoteDebugPort) {
|
||||
used[row.PreferredRemoteDebugPort] = struct{}{}
|
||||
}
|
||||
if domain.ValidRemoteDebugPort(row.RemoteDebugPort) {
|
||||
used[row.RemoteDebugPort] = struct{}{}
|
||||
}
|
||||
}
|
||||
return s.nextUnreservedRemoteDebugPort(used)
|
||||
}
|
||||
|
||||
func (s *Shell) nextUnreservedRemoteDebugPort(used map[int]struct{}) int {
|
||||
start, err := normalizeRemoteDebugStartPort(s.remoteDebugPort.Text())
|
||||
if err != nil {
|
||||
start = domain.DefaultRemoteDebugPort
|
||||
}
|
||||
for port := start; port <= domain.MaxRemoteDebugPort; port++ {
|
||||
if _, exists := used[port]; !exists {
|
||||
return port
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *Shell) remoteDebugPortReservedByOther(port int, excludeID string) bool {
|
||||
if !domain.ValidRemoteDebugPort(port) {
|
||||
return false
|
||||
}
|
||||
for _, row := range s.rows {
|
||||
if row.ID == excludeID {
|
||||
continue
|
||||
}
|
||||
if row.PreferredRemoteDebugPort == port || row.RemoteDebugPort == port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Shell) reservedRemoteDebugPorts(excludeID string) []int {
|
||||
reserved := make([]int, 0, len(s.rows)*2)
|
||||
seen := make(map[int]struct{}, len(s.rows)*2)
|
||||
for _, row := range s.rows {
|
||||
if row.ID == excludeID {
|
||||
continue
|
||||
}
|
||||
for _, port := range []int{row.PreferredRemoteDebugPort, row.RemoteDebugPort} {
|
||||
if !domain.ValidRemoteDebugPort(port) {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[port]; exists {
|
||||
continue
|
||||
}
|
||||
seen[port] = struct{}{}
|
||||
reserved = append(reserved, port)
|
||||
}
|
||||
}
|
||||
return reserved
|
||||
}
|
||||
|
||||
func (s *Shell) notifyInstancesChanged() {
|
||||
if s.onInstancesChanged != nil {
|
||||
s.onInstancesChanged(append([]InstanceRow(nil), s.rows...))
|
||||
|
||||
Reference in New Issue
Block a user