feat: add local cdp startup and external association
This commit is contained in:
+229
-79
@@ -6,8 +6,10 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"chub/internal/domain"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
@@ -26,11 +28,12 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
instanceNameColumnWeight = 24
|
||||
instanceBrowserColumnWeight = 12
|
||||
instanceDirectoryColumnWeight = 38
|
||||
instanceStatusColumnWeight = 12
|
||||
instanceActionColumnWeight = 14
|
||||
instanceNameColumnWeight = 20
|
||||
instanceBrowserColumnWeight = 10
|
||||
instanceDirectoryColumnWeight = 32
|
||||
instancePortColumnWeight = 10
|
||||
instanceStatusColumnWeight = 13
|
||||
instanceActionColumnWeight = 15
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -51,7 +54,15 @@ type PathSearcher func(context.Context, PathField, string) (string, error)
|
||||
|
||||
type DirectoryChooser func(context.Context) (string, error)
|
||||
|
||||
type InstanceStarter func(context.Context, InstanceRow, SettingsState) (int, error)
|
||||
type InstanceStartOutcome struct {
|
||||
PID int
|
||||
RemoteDebugPort int
|
||||
External bool
|
||||
Source string
|
||||
Warning string
|
||||
}
|
||||
|
||||
type InstanceStarter func(context.Context, InstanceRow, SettingsState) (InstanceStartOutcome, error)
|
||||
|
||||
type instanceStartState struct {
|
||||
request uint64
|
||||
@@ -61,7 +72,7 @@ type instanceStartState struct {
|
||||
type instanceStartResult struct {
|
||||
id string
|
||||
request uint64
|
||||
pid int
|
||||
outcome InstanceStartOutcome
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -93,20 +104,24 @@ type directoryPickResult struct {
|
||||
// InstanceRow is the read-only view model used by the first Gio shell.
|
||||
// Runtime data will replace these fixtures when the application service is wired.
|
||||
type InstanceRow struct {
|
||||
ID string
|
||||
Name string
|
||||
Browser string
|
||||
UserDataDir string
|
||||
TargetURL string
|
||||
Status string
|
||||
ID string
|
||||
Name string
|
||||
Browser string
|
||||
UserDataDir string
|
||||
TargetURL string
|
||||
PID int
|
||||
RemoteDebugPort int
|
||||
OccupancySource string
|
||||
Status string
|
||||
}
|
||||
|
||||
type SettingsState struct {
|
||||
ChromePath string
|
||||
EdgePath string
|
||||
DefaultDir string
|
||||
LogDir string
|
||||
CloseOnExit bool
|
||||
ChromePath string
|
||||
EdgePath string
|
||||
DefaultDir string
|
||||
LogDir string
|
||||
RemoteDebugStartPort int
|
||||
CloseOnExit bool
|
||||
}
|
||||
|
||||
// Shell owns every interactive Gio widget. Keeping this state outside Layout
|
||||
@@ -133,17 +148,18 @@ type Shell struct {
|
||||
saveClick widget.Clickable
|
||||
cancelClick widget.Clickable
|
||||
|
||||
chromePath widget.Editor
|
||||
edgePath widget.Editor
|
||||
dataDir widget.Editor
|
||||
logDir widget.Editor
|
||||
closeOnExit widget.Bool
|
||||
instanceName widget.Editor
|
||||
instanceDir widget.Editor
|
||||
instanceURL widget.Editor
|
||||
browserKind widget.Enum
|
||||
pathFeedback string
|
||||
formFeedback string
|
||||
chromePath widget.Editor
|
||||
edgePath widget.Editor
|
||||
dataDir widget.Editor
|
||||
logDir widget.Editor
|
||||
remoteDebugPort widget.Editor
|
||||
closeOnExit widget.Bool
|
||||
instanceName widget.Editor
|
||||
instanceDir widget.Editor
|
||||
instanceURL widget.Editor
|
||||
browserKind widget.Enum
|
||||
pathFeedback string
|
||||
formFeedback string
|
||||
|
||||
list widget.List
|
||||
rows []InstanceRow
|
||||
@@ -171,9 +187,9 @@ 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`, Status: "运行中"},
|
||||
{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`, 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: "已退出"},
|
||||
}, searches: map[PathField]*pathSearchState{
|
||||
PathChromeExecutable: {},
|
||||
@@ -185,6 +201,7 @@ func NewShell(theme *material.Theme) *Shell {
|
||||
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
||||
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
|
||||
s.logDir.SetText(`C:\Users\Public\chub\logs`)
|
||||
s.remoteDebugPort.SetText(strconv.Itoa(domain.DefaultRemoteDebugPort))
|
||||
s.closeOnExit.Value = true
|
||||
s.browserKind.Value = "chrome"
|
||||
s.list.Axis = layout.Vertical
|
||||
@@ -206,6 +223,11 @@ func (s *Shell) SetSettings(value SettingsState) {
|
||||
s.edgePath.SetText(value.EdgePath)
|
||||
s.dataDir.SetText(value.DefaultDir)
|
||||
s.logDir.SetText(value.LogDir)
|
||||
port := value.RemoteDebugStartPort
|
||||
if port == 0 {
|
||||
port = domain.DefaultRemoteDebugPort
|
||||
}
|
||||
s.remoteDebugPort.SetText(strconv.Itoa(port))
|
||||
s.closeOnExit.Value = value.CloseOnExit
|
||||
}
|
||||
|
||||
@@ -323,39 +345,77 @@ func (s *Shell) instances(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout),
|
||||
layout.Rigid(material.Button(s.theme, &s.newClick, "新建实例").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
|
||||
layout.Rigid(s.instanceHeader),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return material.List(s.theme, &s.list).Layout(gtx, len(s.rows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
row := s.rows[i]
|
||||
return layout.Inset{Right: unit.Dp(4), Bottom: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
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(instanceStatusColumnWeight, statusLabel(s.theme, row.Status)),
|
||||
layout.Flexed(instanceActionColumnWeight, s.instanceActionButtons(row)),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
layout.Flexed(1, s.instanceTable),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(material.Caption(s.theme, s.instanceFeedback).Layout),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) instanceTable(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
rect := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
if rect.Empty() {
|
||||
return layout.Dimensions{Size: rect.Max}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color.NRGBA{R: 199, G: 207, B: 218, A: 255}, clip.UniformRRect(rect, gtx.Dp(8)).Op(gtx.Ops))
|
||||
inner := rect.Inset(gtx.Dp(1))
|
||||
if !inner.Empty() {
|
||||
paint.FillShape(gtx.Ops, s.theme.Palette.Bg, clip.UniformRRect(inner, gtx.Dp(7)).Op(gtx.Ops))
|
||||
}
|
||||
return layout.Dimensions{Size: rect.Max}
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Top: unit.Dp(8), Right: unit.Dp(10), Bottom: unit.Dp(8), Left: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(s.instanceHeader),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return material.List(s.theme, &s.list).Layout(gtx, len(s.rows), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
row := s.rows[i]
|
||||
return layout.Inset{Bottom: unit.Dp(8)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
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(instanceStatusColumnWeight, statusLabel(s.theme, row.Status)),
|
||||
layout.Flexed(instanceActionColumnWeight, s.instanceActionButtons(row)),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) instanceHeader(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Right: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(instanceNameColumnWeight, material.Caption(s.theme, "实例名称").Layout),
|
||||
layout.Flexed(instanceBrowserColumnWeight, material.Caption(s.theme, "浏览器类型").Layout),
|
||||
layout.Flexed(instanceDirectoryColumnWeight, material.Caption(s.theme, "用户数据目录").Layout),
|
||||
layout.Flexed(instanceStatusColumnWeight, material.Caption(s.theme, "状态").Layout),
|
||||
layout.Flexed(instanceActionColumnWeight, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.E.Layout(gtx, material.Caption(s.theme, "操作").Layout)
|
||||
}),
|
||||
)
|
||||
})
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
rect := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
if rect.Dy() > 0 {
|
||||
line := image.Rect(rect.Min.X, rect.Max.Y-gtx.Dp(1), rect.Max.X, rect.Max.Y)
|
||||
paint.FillShape(gtx.Ops, color.NRGBA{R: 220, G: 226, B: 235, A: 255}, clip.Rect{Min: line.Min, Max: line.Max}.Op())
|
||||
}
|
||||
return layout.Dimensions{Size: rect.Max}
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Flexed(instanceNameColumnWeight, material.Caption(s.theme, "实例名称").Layout),
|
||||
layout.Flexed(instanceBrowserColumnWeight, material.Caption(s.theme, "浏览器类型").Layout),
|
||||
layout.Flexed(instanceDirectoryColumnWeight, material.Caption(s.theme, "用户数据目录").Layout),
|
||||
layout.Flexed(instancePortColumnWeight, material.Caption(s.theme, "调试端口").Layout),
|
||||
layout.Flexed(instanceStatusColumnWeight, material.Caption(s.theme, "状态").Layout),
|
||||
layout.Flexed(instanceActionColumnWeight, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.E.Layout(gtx, material.Caption(s.theme, "操作").Layout)
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func pathCell(theme *material.Theme, path string) layout.Widget {
|
||||
@@ -364,11 +424,23 @@ func pathCell(theme *material.Theme, path string) layout.Widget {
|
||||
return style.Layout
|
||||
}
|
||||
|
||||
func remoteDebugPortCell(theme *material.Theme, port int) layout.Widget {
|
||||
label := "—"
|
||||
if port > 0 {
|
||||
label = strconv.Itoa(port)
|
||||
}
|
||||
return material.Body2(theme, label).Layout
|
||||
}
|
||||
|
||||
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 {
|
||||
actionLabel := "启动 " + row.Name
|
||||
if row.Status == "外部已关联" {
|
||||
actionLabel = "重新检测 " + row.Name
|
||||
}
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(s.instanceIconButton(s.startClickFor(row.ID), instanceStartIcon, "启动 "+row.Name, s.theme.Palette.ContrastBg)),
|
||||
layout.Rigid(s.instanceIconButton(s.startClickFor(row.ID), instanceStartIcon, actionLabel, s.theme.Palette.ContrastBg)),
|
||||
layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(s.instanceIconButton(s.deleteClickFor(row.ID), instanceDeleteIcon, "删除 "+row.Name, color.NRGBA{R: 188, G: 51, B: 51, A: 255})),
|
||||
)
|
||||
@@ -454,11 +526,14 @@ func statusLabel(theme *material.Theme, status string) layout.Widget {
|
||||
style := material.Label(theme, unit.Sp(13), status)
|
||||
style.Color = theme.Palette.Fg
|
||||
if color, ok := map[string]color.NRGBA{
|
||||
"运行中": {R: 24, G: 125, B: 78, A: 255},
|
||||
"启动中": {R: 175, G: 105, B: 0, A: 255},
|
||||
"启动失败": {R: 188, G: 51, B: 51, A: 255},
|
||||
"外部占用": {R: 190, G: 75, B: 35, A: 255},
|
||||
"已退出": {R: 100, G: 105, B: 115, A: 255},
|
||||
"运行中": {R: 24, G: 125, B: 78, A: 255},
|
||||
"启动中": {R: 175, G: 105, B: 0, A: 255},
|
||||
"启动失败": {R: 188, G: 51, B: 51, A: 255},
|
||||
"运行中(调试不可用)": {R: 175, G: 105, B: 0, A: 255},
|
||||
"外部已关联": {R: 146, G: 93, B: 0, A: 255},
|
||||
"外部占用": {R: 190, G: 75, B: 35, A: 255},
|
||||
"未知占用": {R: 190, G: 75, B: 35, A: 255},
|
||||
"已退出": {R: 100, G: 105, B: 115, A: 255},
|
||||
}[status]; ok {
|
||||
style.Color = color
|
||||
}
|
||||
@@ -491,8 +566,14 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
s.togglePathSearch(PathLogDirectory)
|
||||
}
|
||||
for s.saveClick.Clicked(gtx) {
|
||||
settings, err := s.settingsState()
|
||||
if err != nil {
|
||||
s.pathFeedback = err.Error()
|
||||
continue
|
||||
}
|
||||
s.remoteDebugPort.SetText(strconv.Itoa(settings.RemoteDebugStartPort))
|
||||
if s.onSave != nil {
|
||||
s.onSave(SettingsState{ChromePath: s.chromePath.Text(), EdgePath: s.edgePath.Text(), DefaultDir: s.dataDir.Text(), LogDir: s.logDir.Text(), CloseOnExit: s.closeOnExit.Value})
|
||||
s.onSave(settings)
|
||||
}
|
||||
}
|
||||
for s.cancelClick.Clicked(gtx) {
|
||||
@@ -510,6 +591,8 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(s.pathField(PathLogDirectory, "日志目录", &s.logDir, &s.logPick, &s.logSearch)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(s.remoteDebugPortField),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(material.Caption(s.theme, s.pathFeedback).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||
layout.Rigid(material.CheckBox(s.theme, &s.closeOnExit, "退出时关闭托管浏览器").Layout),
|
||||
@@ -524,6 +607,15 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) remoteDebugPortField(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.Body2(s.theme, "远程调试起始端口").Layout),
|
||||
layout.Rigid(material.Caption(s.theme, "留空时使用 9666;仅绑定 127.0.0.1,启动时顺序尝试下一个可用端口").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(material.Editor(s.theme, &s.remoteDebugPort, "例如:9666").Layout),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Shell) pathField(field PathField, label string, editor *widget.Editor, pick, search *widget.Clickable) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
@@ -758,6 +850,11 @@ func (s *Shell) requestStart(id string) {
|
||||
s.instanceFeedback = "浏览器启动服务尚未准备好。"
|
||||
return
|
||||
}
|
||||
settings, err := s.settingsState()
|
||||
if err != nil {
|
||||
s.instanceFeedback = err.Error()
|
||||
return
|
||||
}
|
||||
if state == nil {
|
||||
state = &instanceStartState{}
|
||||
s.startStates[id] = state
|
||||
@@ -767,10 +864,9 @@ func (s *Shell) requestStart(id string) {
|
||||
state.running = true
|
||||
s.setInstanceStatus(id, "启动中")
|
||||
s.instanceFeedback = fmt.Sprintf("正在启动 %s…", row.Name)
|
||||
settings := s.settingsState()
|
||||
go func() {
|
||||
pid, err := s.instanceStarter(context.Background(), row, settings)
|
||||
s.startResults <- instanceStartResult{id: id, request: request, pid: pid, err: err}
|
||||
outcome, err := s.instanceStarter(context.Background(), row, settings)
|
||||
s.startResults <- instanceStartResult{id: id, request: request, outcome: outcome, err: err}
|
||||
if s.invalidate != nil {
|
||||
s.invalidate()
|
||||
}
|
||||
@@ -791,12 +887,35 @@ func (s *Shell) consumeStartResults() {
|
||||
continue
|
||||
}
|
||||
if result.err != nil {
|
||||
s.setInstanceStatus(result.id, "启动失败")
|
||||
s.instanceFeedback = fmt.Sprintf("启动 %s 失败:%v", row.Name, result.err)
|
||||
if errors.Is(result.err, domain.ErrProfileOccupied) {
|
||||
if result.outcome.Source == "chub_registry" {
|
||||
s.setInstanceRuntime(result.id, "运行中", result.outcome.PID, result.outcome.RemoteDebugPort, result.outcome.Source)
|
||||
s.instanceFeedback = fmt.Sprintf("%s 已由 Chub 启动,未重复创建浏览器。", row.Name)
|
||||
} else {
|
||||
s.setInstanceRuntime(result.id, "外部占用", result.outcome.PID, result.outcome.RemoteDebugPort, result.outcome.Source)
|
||||
s.instanceFeedback = fmt.Sprintf("%s 的 User Data Dir 已被外部浏览器占用,未再次启动。", row.Name)
|
||||
}
|
||||
} else {
|
||||
s.setInstanceStatus(result.id, "启动失败")
|
||||
s.instanceFeedback = fmt.Sprintf("启动 %s 失败:%v", row.Name, result.err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.setInstanceStatus(result.id, "运行中")
|
||||
s.instanceFeedback = fmt.Sprintf("已启动 %s(PID %d)。", row.Name, result.pid)
|
||||
if result.outcome.External {
|
||||
s.setInstanceRuntime(result.id, "外部已关联", result.outcome.PID, result.outcome.RemoteDebugPort, result.outcome.Source)
|
||||
s.instanceFeedback = fmt.Sprintf("已关联外部 %s(PID %d,端口 %d);不会接管其生命周期。", row.Name, result.outcome.PID, result.outcome.RemoteDebugPort)
|
||||
continue
|
||||
}
|
||||
status := "运行中"
|
||||
if result.outcome.Warning != "" {
|
||||
status = "运行中(调试不可用)"
|
||||
}
|
||||
s.setInstanceRuntime(result.id, status, result.outcome.PID, result.outcome.RemoteDebugPort, "chub_registry")
|
||||
if result.outcome.Warning != "" {
|
||||
s.instanceFeedback = fmt.Sprintf("%s 已启动(PID %d),但%s。", row.Name, result.outcome.PID, result.outcome.Warning)
|
||||
} else {
|
||||
s.instanceFeedback = fmt.Sprintf("已启动 %s(PID %d,端口 %d)。", row.Name, result.outcome.PID, result.outcome.RemoteDebugPort)
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -875,14 +994,44 @@ func (s *Shell) setInstanceStatus(id, status string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Shell) settingsState() SettingsState {
|
||||
return SettingsState{
|
||||
ChromePath: s.chromePath.Text(),
|
||||
EdgePath: s.edgePath.Text(),
|
||||
DefaultDir: s.dataDir.Text(),
|
||||
LogDir: s.logDir.Text(),
|
||||
CloseOnExit: s.closeOnExit.Value,
|
||||
func (s *Shell) setInstanceRuntime(id, status string, pid, remoteDebugPort int, source string) bool {
|
||||
for i := range s.rows {
|
||||
if s.rows[i].ID == id {
|
||||
s.rows[i].Status = status
|
||||
s.rows[i].PID = pid
|
||||
s.rows[i].RemoteDebugPort = remoteDebugPort
|
||||
s.rows[i].OccupancySource = source
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Shell) settingsState() (SettingsState, error) {
|
||||
port, err := normalizeRemoteDebugStartPort(s.remoteDebugPort.Text())
|
||||
if err != nil {
|
||||
return SettingsState{}, err
|
||||
}
|
||||
return SettingsState{
|
||||
ChromePath: s.chromePath.Text(),
|
||||
EdgePath: s.edgePath.Text(),
|
||||
DefaultDir: s.dataDir.Text(),
|
||||
LogDir: s.logDir.Text(),
|
||||
RemoteDebugStartPort: port,
|
||||
CloseOnExit: s.closeOnExit.Value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeRemoteDebugStartPort(value string) (int, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return domain.DefaultRemoteDebugPort, nil
|
||||
}
|
||||
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) notifyInstancesChanged() {
|
||||
@@ -964,4 +1113,5 @@ func (s *Shell) resetSettings() {
|
||||
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
|
||||
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
|
||||
s.logDir.SetText(`C:\Users\Public\chub\logs`)
|
||||
s.remoteDebugPort.SetText(strconv.Itoa(domain.DefaultRemoteDebugPort))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user