Browse Source

update testing packages.

ZRY 1 month ago
parent
commit
515acd1c31
30 changed files with 1673 additions and 0 deletions
  1. 49 0
      pkgtest/go.mod
  2. 124 0
      pkgtest/go.sum
  3. 80 0
      pkgtest/pmtest-client/pmtest-client/.gitignore
  4. BIN
      pkgtest/pmtest-client/pmtest-client/.vs/ProjectEvaluation/pmtest-client.metadata.v6.1
  5. BIN
      pkgtest/pmtest-client/pmtest-client/.vs/ProjectEvaluation/pmtest-client.projects.v6.1
  6. BIN
      pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/DesignTimeBuild/.dtbcache.v2
  7. BIN
      pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/FileContentIndex/1fe87a4f-bb42-4dab-842d-483779166247.vsidx
  8. 0 0
      pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/FileContentIndex/read.lock
  9. 16 0
      pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/project-colors.json
  10. BIN
      pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/v17/.futdcache.v1
  11. BIN
      pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/v17/.futdcache.v2
  12. 9 0
      pkgtest/pmtest-client/pmtest-client/App.xaml
  13. 17 0
      pkgtest/pmtest-client/pmtest-client/App.xaml.cs
  14. 10 0
      pkgtest/pmtest-client/pmtest-client/AssemblyInfo.cs
  15. 74 0
      pkgtest/pmtest-client/pmtest-client/CommandLineSplit.cs
  16. 46 0
      pkgtest/pmtest-client/pmtest-client/JsonDef.cs
  17. 165 0
      pkgtest/pmtest-client/pmtest-client/MainWindow.xaml
  18. 349 0
      pkgtest/pmtest-client/pmtest-client/MainWindow.xaml.cs
  19. 16 0
      pkgtest/pmtest-client/pmtest-client/pmtest-client.csproj
  20. 25 0
      pkgtest/pmtest-client/pmtest-client/pmtest-client.sln
  21. 22 0
      pkgtest/pmtest-server/api_jsondef.go
  22. 47 0
      pkgtest/pmtest-server/main.go
  23. 16 0
      pkgtest/pmtest-server/pm_wrapper.go
  24. 207 0
      pkgtest/pmtest-server/web.go
  25. 11 0
      pkgtest/run-group-test-2023-0125-0410/main.go
  26. 52 0
      pkgtest/run-group-test-2023-0125-0410/task.go
  27. 130 0
      pkgtest/run-group-test-2023-0125-0410/ui.go
  28. 83 0
      pkgtest/test-app-a/main.go
  29. 75 0
      pkgtest/tomb-test-2023-0124-1301/main.go
  30. 50 0
      pkgtest/ws-client-test/ws-client-test.go

+ 49 - 0
pkgtest/go.mod

@@ -0,0 +1,49 @@
+module git.swzry.com/zry/ran-proc/pkgtest
+
+go 1.19
+
+require (
+	git.swzry.com/zry/GoHiedaLogger/hieda_ginutil v0.0.0-20230124221017-3a7a6ec97a0d
+	git.swzry.com/zry/GoHiedaLogger/hiedabke_console v0.0.0-20221201004124-970fc945a7b7
+	git.swzry.com/zry/GoHiedaLogger/hiedalog v0.0.0-20221201004124-970fc945a7b7
+	git.swzry.com/zry/go-lazy-quiter v0.0.0-20230314224806-9477d49516ef
+	github.com/gdamore/tcell/v2 v2.5.3
+	github.com/gin-gonic/gin v1.9.0
+	github.com/hashicorp/go-uuid v1.0.3
+	github.com/liushuochen/gotable v0.0.0-20221119160816-1113793e7092
+	github.com/oklog/run v1.1.0
+	github.com/rivo/tview v0.0.0-20230104153304-892d1a2eb0da
+	gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
+)
+
+require (
+	github.com/bytedance/sonic v1.8.0 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+	github.com/gdamore/encoding v1.0.0 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-playground/validator/v10 v10.11.2 // indirect
+	github.com/goccy/go-json v0.10.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.9 // indirect
+	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mattn/go-runewidth v0.0.13 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/rivo/uniseg v0.4.2 // indirect
+	github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.9 // indirect
+	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
+	golang.org/x/crypto v0.5.0 // indirect
+	golang.org/x/net v0.7.0 // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/term v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
+	google.golang.org/protobuf v1.28.1 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 124 - 0
pkgtest/go.sum

@@ -0,0 +1,124 @@
+git.swzry.com/zry/GoHiedaLogger/hieda_ginutil v0.0.0-20230124221017-3a7a6ec97a0d h1:M8UJmnOkiCCmum8tYMAO6lLensLoJRzGcj2G15OIla0=
+git.swzry.com/zry/GoHiedaLogger/hieda_ginutil v0.0.0-20230124221017-3a7a6ec97a0d/go.mod h1:P+s0sqwU9FFLYxE6fbDRL8wbmbennR8pBkdQh0/b4j4=
+git.swzry.com/zry/GoHiedaLogger/hiedabke_console v0.0.0-20221201004124-970fc945a7b7 h1:62xWzE/n4PFmCunRU0P09JPZcuznUo4io7hK0hfKiC4=
+git.swzry.com/zry/GoHiedaLogger/hiedabke_console v0.0.0-20221201004124-970fc945a7b7/go.mod h1:QKGs1+W8+y6/RMTSaAv6LwJnY9/7zaGs38IbwmYlFbo=
+git.swzry.com/zry/GoHiedaLogger/hiedalog v0.0.0-20221201004124-970fc945a7b7 h1:EkaA9qf1XK4g7//o0cQTCc+zvz1fdvEXPDQWIvd7EoI=
+git.swzry.com/zry/GoHiedaLogger/hiedalog v0.0.0-20221201004124-970fc945a7b7/go.mod h1:NMU7558kNXCUuK0qKYQMtYK/kn2lhwelnij295H3pdU=
+git.swzry.com/zry/go-lazy-quiter v0.0.0-20200424062628-4597608cc939 h1:tlGUJJMHC3uZYvmoEAf07j0Vjzj1LeQTHHr5Tg/LvU8=
+git.swzry.com/zry/go-lazy-quiter v0.0.0-20200424062628-4597608cc939/go.mod h1:YLfmmWuUKgksSvCAUd0Qz+fYkaE00Kw03y+2sg4rPzk=
+git.swzry.com/zry/go-lazy-quiter v0.0.0-20230314224047-37f6d607af34 h1:YacVCZx9STrlFXWw9Ed5mB/Pt7x38pM239yxnwzEHgo=
+git.swzry.com/zry/go-lazy-quiter v0.0.0-20230314224047-37f6d607af34/go.mod h1:eZ9m10j5Jyx/ss9rR+njRozBr+3pn6vnLGZdSqZrGSE=
+git.swzry.com/zry/go-lazy-quiter v0.0.0-20230314224806-9477d49516ef h1:nUXmES3nWAIvcbTYCXSM9d3T2WyUUM+iCXYt6YKprPg=
+git.swzry.com/zry/go-lazy-quiter v0.0.0-20230314224806-9477d49516ef/go.mod h1:eZ9m10j5Jyx/ss9rR+njRozBr+3pn6vnLGZdSqZrGSE=
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
+github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
+github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
+github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
+github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
+github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
+github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
+github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
+github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/liushuochen/gotable v0.0.0-20221119160816-1113793e7092 h1:u9I3sJ+uTakxnRrvuYJGsEi4SvEMN+yB47WWGDHHxIk=
+github.com/liushuochen/gotable v0.0.0-20221119160816-1113793e7092/go.mod h1:CxUy8nDvutaC1pOfaG9TRoYwdHHqoNstSPPKhomC9k8=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
+github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
+github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
+github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/tview v0.0.0-20230104153304-892d1a2eb0da h1:3Mh+tcC2KqetuHpWMurDeF+yOgyt4w4qtLIpwSQ3uqo=
+github.com/rivo/tview v0.0.0-20230104153304-892d1a2eb0da/go.mod h1:lBUy/T5kyMudFzWUH/C2moN+NlU5qF505vzOyINXuUQ=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
+github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4=
+github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
+github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
+golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
+gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 80 - 0
pkgtest/pmtest-client/pmtest-client/.gitignore

@@ -0,0 +1,80 @@
+# Build and Object Folders
+bin/
+obj/
+
+# Nuget packages directory
+packages/
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Rr]elease/
+x64/
+*_i.c
+*_p.c
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.log
+*.vspscc
+*.vssscc
+.builds
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*
+
+# NCrunch
+*.ncrunch*
+.*crunch*.local.xml
+
+# Installshield output folder
+[Ee]xpress
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help
+UpgradeLog*.XML
+
+# Lightswitch
+_Pvt_Extensions
+GeneratedArtifacts
+*.xap
+ModelManifest.xml
+
+#Backup file
+*.bak
+
+/bin/
+/obj/

BIN
pkgtest/pmtest-client/pmtest-client/.vs/ProjectEvaluation/pmtest-client.metadata.v6.1


BIN
pkgtest/pmtest-client/pmtest-client/.vs/ProjectEvaluation/pmtest-client.projects.v6.1


BIN
pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/DesignTimeBuild/.dtbcache.v2


BIN
pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/FileContentIndex/1fe87a4f-bb42-4dab-842d-483779166247.vsidx


+ 0 - 0
pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/FileContentIndex/read.lock


+ 16 - 0
pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/project-colors.json

@@ -0,0 +1,16 @@
+{
+  "Version": 1,
+  "ProjectMap": {
+    "54d9e3a2-62e6-4a89-9aca-ee09833e8343": {
+      "ProjectGuid": "54d9e3a2-62e6-4a89-9aca-ee09833e8343",
+      "DisplayName": "pmtest-client",
+      "ColorIndex": 0
+    },
+    "a2fe74e1-b743-11d0-ae1a-00a0c90fffc3": {
+      "ProjectGuid": "a2fe74e1-b743-11d0-ae1a-00a0c90fffc3",
+      "DisplayName": "杂项文件",
+      "ColorIndex": -1
+    }
+  },
+  "NextColorIndex": 1
+}

BIN
pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/v17/.futdcache.v1


BIN
pkgtest/pmtest-client/pmtest-client/.vs/pmtest-client/v17/.futdcache.v2


+ 9 - 0
pkgtest/pmtest-client/pmtest-client/App.xaml

@@ -0,0 +1,9 @@
+<Application x:Class="pmtest_client.App"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:pmtest_client"
+             StartupUri="MainWindow.xaml">
+    <Application.Resources>
+         
+    </Application.Resources>
+</Application>

+ 17 - 0
pkgtest/pmtest-client/pmtest-client/App.xaml.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace pmtest_client
+{
+    /// <summary>
+    /// Interaction logic for App.xaml
+    /// </summary>
+    public partial class App : Application
+    {
+    }
+}

+ 10 - 0
pkgtest/pmtest-client/pmtest-client/AssemblyInfo.cs

@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+                                     //(used if a resource is not found in the page,
+                                     // or application resource dictionaries)
+    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+                                              //(used if a resource is not found in the page,
+                                              // app, or any theme specific resource dictionaries)
+)]

+ 74 - 0
pkgtest/pmtest-client/pmtest-client/CommandLineSplit.cs

@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace pmtest_client
+{
+    public class CommandLineSplit
+    {
+        public static IEnumerable<string> SplitArgs(string commandLine)
+        {
+            var result = new StringBuilder();
+
+            var quoted = false;
+            var escaped = false;
+            var started = false;
+            var allowcaret = false;
+            for (int i = 0; i < commandLine.Length; i++)
+            {
+                var chr = commandLine[i];
+
+                if (chr == '^' && !quoted)
+                {
+                    if (allowcaret)
+                    {
+                        result.Append(chr);
+                        started = true;
+                        escaped = false;
+                        allowcaret = false;
+                    }
+                    else if (i + 1 < commandLine.Length && commandLine[i + 1] == '^')
+                    {
+                        allowcaret = true;
+                    }
+                    else if (i + 1 == commandLine.Length)
+                    {
+                        result.Append(chr);
+                        started = true;
+                        escaped = false;
+                    }
+                }
+                else if (escaped)
+                {
+                    result.Append(chr);
+                    started = true;
+                    escaped = false;
+                }
+                else if (chr == '"')
+                {
+                    quoted = !quoted;
+                    started = true;
+                }
+                else if (chr == '\\' && i + 1 < commandLine.Length && commandLine[i + 1] == '"')
+                {
+                    escaped = true;
+                }
+                else if (chr == ' ' && !quoted)
+                {
+                    if (started) yield return result.ToString();
+                    result.Clear();
+                    started = false;
+                }
+                else
+                {
+                    result.Append(chr);
+                    started = true;
+                }
+            }
+
+            if (started) yield return result.ToString();
+        }
+    }
+}

+ 46 - 0
pkgtest/pmtest-client/pmtest-client/JsonDef.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace pmtest_client
+{
+    public class JsonDef_Req_CreateTask
+    {
+        [JsonPropertyName("taskName")]
+        public string TaskName { get; set; }
+        [JsonPropertyName("executablePath")]
+        public string ExecutablePath { get; set; }
+        [JsonPropertyName("args")]
+        public IEnumerable<string> Args { get; set; }
+        [JsonPropertyName("workDir")]
+        public string WorkDir { get; set; }
+        [JsonPropertyName("execTimeout")]
+        public int ExecTimeout { get; set; }
+    }
+
+    public class JsonDef_Req_CreateDaemon
+    {
+        [JsonPropertyName("daemonName")]
+        public string DaemonName { get; set; }
+        [JsonPropertyName("executablePath")]
+        public string ExecutablePath { get; set; }
+        [JsonPropertyName("args")]
+        public IEnumerable<string> Args { get; set; }
+        [JsonPropertyName("workDir")]
+        public string WorkDir { get; set; }
+        [JsonPropertyName("enableAfterCreate")]
+        public bool EnableAfterCreate { get; set; }
+    }
+
+    public class JsonDef_Req_SetDaemonEnable
+    {
+        [JsonPropertyName("cpid")]
+        public string CPID { get; set; }
+        [JsonPropertyName("enable")]
+        public bool Enable { get; set; }
+    }
+}

+ 165 - 0
pkgtest/pmtest-client/pmtest-client/MainWindow.xaml

@@ -0,0 +1,165 @@
+<Window x:Class="pmtest_client.MainWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:pmtest_client"
+        mc:Ignorable="d"
+        Title="MainWindow" Height="800" Width="1600">
+    <Grid>
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition />
+            <ColumnDefinition Width="600"/>
+        </Grid.ColumnDefinitions>
+        <Grid Grid.Column="0">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="72"/>
+                <RowDefinition />
+                <RowDefinition Height="60"/>
+                <RowDefinition />
+            </Grid.RowDefinitions>
+            <GroupBox Header="服务器" Grid.Row="0">
+                <Grid>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="96"/>
+                        <ColumnDefinition />
+                    </Grid.ColumnDefinitions>
+                    <Grid.RowDefinitions>
+                        <RowDefinition />
+                        <RowDefinition />
+                    </Grid.RowDefinitions>
+                    <Label Grid.Row="0" Grid.Column="0" Content="主机名: " VerticalAlignment="Center"/>
+                    <TextBox Grid.Row="0" x:Name="tbURLRoot" Grid.Column="1" Text="127.0.0.1:9198" VerticalContentAlignment="Center" />
+                    <Label Grid.Row="1" Grid.Column="0" Content="Access Key: " VerticalAlignment="Center"/>
+                    <TextBox Grid.Row="1" x:Name="tbAccessKey" Grid.Column="1" VerticalContentAlignment="Center" />
+                </Grid>
+            </GroupBox>
+            <GroupBox Header="状态" Grid.Row="1">
+                <Grid>
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="32"/>
+                        <RowDefinition />
+                    </Grid.RowDefinitions>
+                    <StackPanel Grid.Row="0" Orientation="Horizontal">
+                        <Label Content="自动刷新   间隔:" VerticalAlignment="Center"/>
+                        <TextBox x:Name="tbAutoRefreshInterval" Width="64" Text="1000" VerticalAlignment="Stretch" VerticalContentAlignment="Center" MaxLines="1"/>
+                        <Button x:Name="btnSetAutoRefresh" Content="启动自动刷新" Width="96" Click="btnSetAutoRefresh_Click" />
+                        <Button x:Name="btnStopAutoRefresh" Content="停止" Width="48" Click="btnStopAutoRefresh_Click" />
+                        <Button x:Name="btnManualRefresh" Content="手动刷新" Width="64" Click="btnManualRefresh_Click" />
+                    </StackPanel>
+                    <TextBox x:Name="tbStatusDisplay" Grid.Row="1" IsReadOnly="True" VerticalScrollBarVisibility="Visible" FontFamily="Cascadia Mono" />
+                </Grid>
+            </GroupBox>
+            <GroupBox Grid.Row="2" Header="进程输出WS连接">
+                <StackPanel Grid.Row="2" Orientation="Horizontal">
+                    <Button x:Name="btnConnectCmdOut" Content="进程输出" Width="72" Click="btnConnectCmdOut_Click" />
+                    <Button x:Name="btnDisconnectCmdOut" Content="断开" Width="48" Click="btnDisconnectCmdOut_Click" />
+                    <Button x:Name="btnClearCmdOut" Content="清空输出" Width="72" Click="btnDisconnectCmdOut_Click" />
+                </StackPanel>
+            </GroupBox>
+            <TabControl Grid.Row="3">
+                <TabItem Header="创建Task">
+                    <Grid>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="96"/>
+                            <ColumnDefinition />
+                            <ColumnDefinition Width="72"/>
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition />
+                        </Grid.RowDefinitions>
+                        <Label Grid.Row="0" Grid.Column="0" Content="任务名称" VerticalContentAlignment="Center" />
+                        <Label Grid.Row="1" Grid.Column="0" Content="可执行文件"  VerticalContentAlignment="Center" />
+                        <Label Grid.Row="2" Grid.Column="0" Content="工作目录"  VerticalContentAlignment="Center" />
+                        <Label Grid.Row="3" Grid.Column="0" Content="参数"  VerticalContentAlignment="Center" />
+                        <Label Grid.Row="4" Grid.Column="0" Content="任务超时"  VerticalContentAlignment="Center" />
+                        <Label Grid.Row="4" Grid.Column="2" Content="ms"  VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbCreateTaskName" VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="1" Grid.Column="1" x:Name="tbCreateTaskExec" VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="2" Grid.Column="1" x:Name="tbCreateTaskWDir" VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="3" Grid.Column="1" x:Name="tbCreateTaskArgs" VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="4" Grid.Column="1" x:Name="tbCreateTaskETmO" VerticalContentAlignment="Center" Text="0" />
+                        <Button Grid.Row="0" Grid.Column="2" x:Name="btnCreateTaskGenNameUUID" Content="生成" Click="btnCreateTaskGenNameUUID_Click"/>
+                        <Button Grid.Row="1" Grid.Column="2" x:Name="btnCreateTaskGenBrowseExec" Content="..." Click="btnCreateTaskGenBrowseExec_Click"/>
+                        <Button Grid.Row="2" Grid.Column="2" x:Name="btnCreateTaskGenBrowseWorkDir" Content="..." Click="btnCreateTaskGenBrowseWorkDir_Click" />
+                        <Button Grid.Row="6" Grid.Column="1" x:Name="btnCreateTaskDo" Content="执行" Click="btnCreateTaskDo_Click"/>
+                    </Grid>
+                </TabItem>
+                <TabItem Header="创建Daemon">
+                    <Grid>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="96"/>
+                            <ColumnDefinition />
+                            <ColumnDefinition Width="72"/>
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition />
+                        </Grid.RowDefinitions>
+                        <Label Grid.Row="0" Grid.Column="0" Content="Daemon名称" VerticalContentAlignment="Center" />
+                        <Label Grid.Row="1" Grid.Column="0" Content="可执行文件"  VerticalContentAlignment="Center" />
+                        <Label Grid.Row="2" Grid.Column="0" Content="工作目录"  VerticalContentAlignment="Center" />
+                        <Label Grid.Row="3" Grid.Column="0" Content="参数"  VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbCreateDaemonName" VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="1" Grid.Column="1" x:Name="tbCreateDaemonExec" VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="2" Grid.Column="1" x:Name="tbCreateDaemonWDir" VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="3" Grid.Column="1" x:Name="tbCreateDaemonArgs" VerticalContentAlignment="Center" />
+                        <CheckBox Grid.Row="4" Grid.Column="1" x:Name="cbCreateDaemonStartAfterC" VerticalContentAlignment="Center" Content="创建后立即启动" IsChecked="True" />
+                        <Button Grid.Row="1" Grid.Column="2" x:Name="btnCreateDaemonBrowseExec" Content="..." Click="btnCreateDaemonBrowseExec_Click"/>
+                        <Button Grid.Row="2" Grid.Column="2" x:Name="btnCreateDaemonBrowseWorkDir" Content="..." Click="btnCreateDaemonBrowseWorkDir_Click" />
+                        <Button Grid.Row="6" Grid.Column="1" x:Name="btnCreateDaemonDo" Content="执行" Click="btnCreateDaemonDo_Click"/>
+                    </Grid>
+                </TabItem>
+                <TabItem Header="启停Daemon">
+                    <Grid>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="96"/>
+                            <ColumnDefinition />
+                            <ColumnDefinition Width="72"/>
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition Height="32" />
+                            <RowDefinition />
+                        </Grid.RowDefinitions>
+                        <Label Grid.Row="0" Grid.Column="0" Content="CPID" VerticalContentAlignment="Center" />
+                        <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbSetDaemonStCPID" VerticalContentAlignment="Center" />
+                        <Button Grid.Row="2" Grid.Column="1" x:Name="btnSetDaemonEnable" Content="启动" Click="btnSetDaemonEnable_Click"/>
+                        <Button Grid.Row="3" Grid.Column="1" x:Name="btnSetDaemonDisable" Content="停止" Click="btnSetDaemonDisable_Click"/>
+                    </Grid>
+                </TabItem>
+            </TabControl>
+        </Grid>
+        <Grid Grid.Column="1">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="24" />
+                <RowDefinition />
+                <RowDefinition Height="24" />
+                <RowDefinition />
+            </Grid.RowDefinitions>
+            <Label Grid.Row="0" Content="操作日志" />
+            <RichTextBox x:Name="rtbLogBox" Grid.Row="1">
+                <FlowDocument>
+                    <Paragraph x:Name="logPara" />
+                </FlowDocument>
+            </RichTextBox>
+            <Label Grid.Row="2" Content="进程输出" />
+            <TextBox x:Name="tbCmdOut" IsReadOnly="True" Grid.Row="3" />
+        </Grid>
+    </Grid>
+</Window>

+ 349 - 0
pkgtest/pmtest-client/pmtest-client/MainWindow.xaml.cs

@@ -0,0 +1,349 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+using System.Net.Http;
+using System.Net.Http.Json;
+using Websocket.Client;
+using Ookii.Dialogs.Wpf;
+using System.Text.Json;
+
+namespace pmtest_client
+{
+    /// <summary>
+    /// Interaction logic for MainWindow.xaml
+    /// </summary>
+    public partial class MainWindow : Window
+    {
+        private System.Timers.Timer autoRefreshTimer;
+        private HttpClient httpClient;
+        private WebsocketClient? wsClient;
+        public MainWindow()
+        {
+            InitializeComponent();
+            autoRefreshTimer = new System.Timers.Timer();
+            autoRefreshTimer.Interval = 1000;
+            autoRefreshTimer.Elapsed += AutoRefreshTimer_Elapsed;
+            autoRefreshTimer.Enabled = false;
+            httpClient = new HttpClient();
+        }
+
+        private void PrintLog(Brush color, String text)
+        {
+            Dispatcher.Invoke(() =>
+            {
+                Run t = new Run("[" + DateTime.Now.ToString() + "] ");
+                t.Foreground = Brushes.Navy;
+                Run tb = new Run(text);
+                tb.Foreground = color;
+                logPara.Inlines.Add(t);
+                logPara.Inlines.Add(tb);
+                logPara.Inlines.Add(new LineBreak());
+                rtbLogBox.ScrollToEnd();
+            });
+        }
+
+        private void AutoRefreshTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
+        {
+            DoStatusRefresh();
+        }
+
+        private void btnSetAutoRefresh_Click(object sender, RoutedEventArgs e)
+        {
+            autoRefreshTimer?.Start();
+        }
+
+        private void btnManualRefresh_Click(object sender, RoutedEventArgs e)
+        {
+            DoStatusRefresh();
+        }
+
+        private void btnStopAutoRefresh_Click(object sender, RoutedEventArgs e)
+        {
+            autoRefreshTimer?.Stop();
+        }
+
+        private void DoStatusRefresh()
+        {
+            Dispatcher.InvokeAsync(async () =>
+            {
+                try
+                {
+                    httpClient.DefaultRequestHeaders.Remove("AuthKey");
+                    httpClient.DefaultRequestHeaders.Add("AuthKey", tbAccessKey.Text);
+                    HttpResponseMessage response = await httpClient.GetAsync(String.Format("http://{0}/", tbURLRoot.Text));
+                    string responseBody = await response.Content.ReadAsStringAsync();
+                    PrintLog(Brushes.LightBlue, "Status refreshed.");
+                    Dispatcher.Invoke(() =>
+                    {
+                        tbStatusDisplay.Text = responseBody;
+                    });
+                }
+                catch (Exception e)
+                {
+                    PrintLog(Brushes.Crimson, String.Format("Failed do status refresh: {0}", e.Message));
+                }
+            });
+        }
+
+        private void btnConnectCmdOut_Click(object sender, RoutedEventArgs e)
+        {
+            if (wsClient == null)
+            {
+                Dispatcher.InvokeAsync(async () =>
+                {
+                    try
+                    {
+                        var uri = new Uri(String.Format("ws://{0}/ws/get-cmd-output.satori?auth-key={1}", tbURLRoot.Text, tbAccessKey.Text));
+                        wsClient = new WebsocketClient(uri);
+                        wsClient.IsReconnectionEnabled = false;
+                        wsClient.MessageReceived.Subscribe(msg =>
+                        {
+                            Dispatcher.Invoke(() =>
+                            {
+                                tbCmdOut.AppendText(msg.Text);
+                            });
+                        });
+                        wsClient.DisconnectionHappened.Subscribe(msg =>
+                        {
+                            PrintLog(Brushes.RoyalBlue, "CmdOutput websocket Disconnected.");
+                            wsClient.Dispose();
+                            wsClient = null;
+                        });
+                        PrintLog(Brushes.RoyalBlue, "CmdOuput websocket connecting...");
+                        await wsClient.Start();
+                        PrintLog(Brushes.RoyalBlue, "CmdOuput websocket connected.");
+                    }
+                    catch (Exception err)
+                    {
+                        PrintLog(Brushes.Crimson, string.Format("Websocket error: {0}", err.Message));
+                    }
+                });
+            }
+
+        }
+
+        private void btnDisconnectCmdOut_Click(object sender, RoutedEventArgs e)
+        {
+            if (wsClient != null)
+            {
+                PrintLog(Brushes.RoyalBlue, "Disconnect CmdOutput websocket connection...");
+                Dispatcher.InvokeAsync(async () =>
+                {
+                    await wsClient.Stop(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, "stop by user.");
+                });
+            }
+        }
+
+        private bool checkApiResponse(Dictionary<string, string> result)
+        {
+            if (result.ContainsKey("status"))
+            {
+                if (result["status"] == "200")
+                {
+                    return true;
+                }
+                else
+                {
+                    if (result.ContainsKey("errMsg"))
+                    {
+                        PrintLog(Brushes.Orange, string.Format("API Error: {0}", result["errMsg"]));
+                        PrintLog(Brushes.PaleVioletRed, serializeResponseJson(result));
+                        return false;
+                    }
+                    else
+                    {
+                        PrintLog(Brushes.Orange, "API Error: Unknown Error");
+                        PrintLog(Brushes.PaleVioletRed, serializeResponseJson(result));
+                        return false;
+                    }
+                }
+            }
+            else
+            {
+                PrintLog(Brushes.Orange, "Invalid response: 'status' field not found.");
+                PrintLog(Brushes.PaleVioletRed, serializeResponseJson(result));
+                return false;
+            }
+        }
+
+        private string serializeResponseJson(Dictionary<string, string> result)
+        {
+            var options = new JsonSerializerOptions { WriteIndented = true };
+            string jsonString = JsonSerializer.Serialize(result, options);
+            return jsonString;
+        }
+        private void btnCreateTaskDo_Click(object sender, RoutedEventArgs e)
+        {
+            JsonDef_Req_CreateTask req = new JsonDef_Req_CreateTask();
+            req.TaskName = tbCreateTaskName.Text;
+            req.ExecutablePath = tbCreateTaskExec.Text;
+            req.WorkDir = tbCreateTaskWDir.Text;
+            req.Args = CommandLineSplit.SplitArgs(tbCreateTaskArgs.Text);
+            int etmo = 0;
+            if (!int.TryParse(tbCreateTaskETmO.Text, out etmo))
+            {
+                etmo = 0;
+            }
+            req.ExecTimeout = etmo;
+            Dispatcher.InvokeAsync(async () =>
+            {
+                try
+                {
+                    httpClient.DefaultRequestHeaders.Remove("AuthKey");
+                    httpClient.DefaultRequestHeaders.Add("AuthKey", tbAccessKey.Text);
+                    HttpResponseMessage response = await httpClient.PostAsJsonAsync<JsonDef_Req_CreateTask>(
+                        String.Format("http://{0}/api/new-task.satori", tbURLRoot.Text),
+                        req
+                    );
+                    Dictionary<string, string> result = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
+                    if (result != null)
+                    {
+                        if (checkApiResponse(result))
+                        {
+                            if (result.ContainsKey("cpid") && result.ContainsKey("name"))
+                            {
+                                PrintLog(Brushes.SeaGreen, string.Format("Task created: CPID={0}, Name={1}", result["cpid"], result["name"]));
+                                btnCreateTaskGenNameUUID_Click(null, null);
+                            }
+                            else
+                            {
+                                PrintLog(Brushes.Orange, "Invalid response: missing some fields.");
+                                PrintLog(Brushes.PaleVioletRed, serializeResponseJson(result));
+                            }
+                        }
+                        else
+                        {
+                            return;
+                        }
+                    }
+                    else
+                    {
+                        PrintLog(Brushes.Orange, "Invalid response: null");
+                    }
+                }
+                catch (Exception e)
+                {
+                    PrintLog(Brushes.Crimson, String.Format("Failed create task: {0}", e.Message));
+                }
+            });
+        }
+
+        private void btnCreateTaskGenBrowseWorkDir_Click(object sender, RoutedEventArgs e)
+        {
+            VistaFolderBrowserDialog fbd = new VistaFolderBrowserDialog();
+            if (fbd.ShowDialog() == true)
+            {
+                tbCreateTaskWDir.Text = fbd.SelectedPath;
+            }
+        }
+
+        private void btnCreateTaskGenBrowseExec_Click(object sender, RoutedEventArgs e)
+        {
+            VistaOpenFileDialog ofd = new VistaOpenFileDialog();
+            ofd.DefaultExt = ".exe";
+            ofd.Filter = "exe files|*.exe|All files|*";
+            if (ofd.ShowDialog() == true)
+            {
+                tbCreateTaskExec.Text = ofd.FileName;
+            }
+        }
+
+        private void btnCreateTaskGenNameUUID_Click(object sender, RoutedEventArgs e)
+        {
+            Guid guid = Guid.NewGuid();
+            tbCreateTaskName.Text = String.Format("task-{0}", guid.ToString());
+        }
+
+        private void btnCreateDaemonBrowseExec_Click(object sender, RoutedEventArgs e)
+        {
+            VistaOpenFileDialog ofd = new VistaOpenFileDialog();
+            ofd.DefaultExt = ".exe";
+            ofd.Filter = "exe files|*.exe|All files|*";
+            if (ofd.ShowDialog() == true)
+            {
+                tbCreateDaemonExec.Text = ofd.FileName;
+            }
+        }
+
+        private void btnCreateDaemonBrowseWorkDir_Click(object sender, RoutedEventArgs e)
+        {
+            VistaFolderBrowserDialog fbd = new VistaFolderBrowserDialog();
+            if (fbd.ShowDialog() == true)
+            {
+                tbCreateDaemonWDir.Text = fbd.SelectedPath;
+            }
+        }
+
+        private void btnCreateDaemonDo_Click(object sender, RoutedEventArgs e)
+        {
+            JsonDef_Req_CreateDaemon req = new JsonDef_Req_CreateDaemon();
+            req.DaemonName = tbCreateDaemonName.Text;
+            req.ExecutablePath = tbCreateDaemonExec.Text;
+            req.WorkDir = tbCreateDaemonWDir.Text;
+            req.Args = CommandLineSplit.SplitArgs(tbCreateDaemonArgs.Text);
+            req.EnableAfterCreate = cbCreateDaemonStartAfterC.IsChecked?.Value;
+            Dispatcher.InvokeAsync(async () =>
+            {
+                try
+                {
+                    httpClient.DefaultRequestHeaders.Remove("AuthKey");
+                    httpClient.DefaultRequestHeaders.Add("AuthKey", tbAccessKey.Text);
+                    HttpResponseMessage response = await httpClient.PostAsJsonAsync<JsonDef_Req_CreateTask>(
+                        String.Format("http://{0}/api/new-task.satori", tbURLRoot.Text),
+                        req
+                    );
+                    Dictionary<string, string> result = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
+                    if (result != null)
+                    {
+                        if (checkApiResponse(result))
+                        {
+                            if (result.ContainsKey("cpid") && result.ContainsKey("name"))
+                            {
+                                PrintLog(Brushes.SeaGreen, string.Format("Task created: CPID={0}, Name={1}", result["cpid"], result["name"]));
+                                btnCreateTaskGenNameUUID_Click(null, null);
+                            }
+                            else
+                            {
+                                PrintLog(Brushes.Orange, "Invalid response: missing some fields.");
+                                PrintLog(Brushes.PaleVioletRed, serializeResponseJson(result));
+                            }
+                        }
+                        else
+                        {
+                            return;
+                        }
+                    }
+                    else
+                    {
+                        PrintLog(Brushes.Orange, "Invalid response: null");
+                    }
+                }
+                catch (Exception e)
+                {
+                    PrintLog(Brushes.Crimson, String.Format("Failed create task: {0}", e.Message));
+                }
+            });
+        }
+
+        private void btnSetDaemonEnable_Click(object sender, RoutedEventArgs e)
+        {
+
+        }
+
+        private void btnSetDaemonDisable_Click(object sender, RoutedEventArgs e)
+        {
+
+        }
+    }
+}

+ 16 - 0
pkgtest/pmtest-client/pmtest-client/pmtest-client.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net6.0-windows</TargetFramework>
+    <RootNamespace>pmtest_client</RootNamespace>
+    <Nullable>enable</Nullable>
+    <UseWPF>true</UseWPF>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
+    <PackageReference Include="Websocket.Client" Version="4.6.1" />
+  </ItemGroup>
+
+</Project>

+ 25 - 0
pkgtest/pmtest-client/pmtest-client/pmtest-client.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32014.148
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pmtest-client", "pmtest-client.csproj", "{54D9E3A2-62E6-4A89-9ACA-EE09833E8343}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{54D9E3A2-62E6-4A89-9ACA-EE09833E8343}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{54D9E3A2-62E6-4A89-9ACA-EE09833E8343}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{54D9E3A2-62E6-4A89-9ACA-EE09833E8343}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{54D9E3A2-62E6-4A89-9ACA-EE09833E8343}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {60AE792C-0BC9-429D-88CD-619F606F2B7B}
+	EndGlobalSection
+EndGlobal

+ 22 - 0
pkgtest/pmtest-server/api_jsondef.go

@@ -0,0 +1,22 @@
+package main
+
+type ApiJsonDef_NewTask struct {
+	TaskName       string   `json:"taskName"`
+	ExecutablePath string   `json:"executablePath"`
+	Args           []string `json:"args"`
+	WorkDir        string   `json:"workDir"`
+	ExecTimeout    int      `json:"execTimeout"`
+}
+
+type ApiJsonDef_NewDaemon struct {
+	DaemonName        string   `json:"daemonName"`
+	ExecutablePath    string   `json:"executablePath"`
+	Args              []string `json:"args"`
+	WorkDir           string   `json:"workDir"`
+	EnableAfterCreate bool     `json:"enableAfterCreate"`
+}
+
+type ApiJsonDef_SetDaemonEnable struct {
+	CPID   string `json:"cpid"`
+	Enable bool   `json:"enable"`
+}

+ 47 - 0
pkgtest/pmtest-server/main.go

@@ -0,0 +1,47 @@
+package main
+
+import (
+	hconsole "git.swzry.com/zry/GoHiedaLogger/hiedabke_console"
+	"git.swzry.com/zry/GoHiedaLogger/hiedalog"
+	lazyquiter "git.swzry.com/zry/go-lazy-quiter"
+	rpcore "git.swzry.com/zry/ran-proc/rpcore"
+	wslogdist "git.swzry.com/zry/ran-proc/wslogdist"
+	"github.com/hashicorp/go-uuid"
+	"github.com/oklog/run"
+	"os"
+)
+
+var Logger *hiedalog.HiedaLogger
+var PM *rpcore.ChildProcManager
+var WebServer *WebServerClass
+var AccessKey string
+var WsLogDistr *wslogdist.WsLogDistributor
+var LazyQuiter *lazyquiter.RunGroupQuiter
+var GracefulShutdownActorForThis rpcore.GracefulShutdownActor
+
+func main() {
+	Logger = hiedalog.NewHiedaLogger()
+	Logger.AddBackend(hconsole.NewConsoleBackend(os.Stdout), Logger.LevelFilter.NameToID(hiedalog.DLN_DEBUG))
+	AccessKey, _ = uuid.GenerateUUID()
+	Logger.LogPrint("key", hiedalog.DLN_INFO, "Access Key:", AccessKey)
+	LazyQuiter = lazyquiter.NewRunGroupQuiter()
+	LazyQuiter.SignalNotifyFunc = func(sig os.Signal) {
+		Logger.LogPrint("main", hiedalog.DLN_INFO, "Quit by user abort.")
+	}
+	GracefulShutdownActorForThis = &rpcore.UnixSignalShutdownActor{}
+	PM = rpcore.NewChildProcManager(&rpcore.DefaultChildProcManagerMsgHandler{
+		Logger:        Logger,
+		LogModuleName: "pm",
+	})
+	WsLogDistr = wslogdist.NewWsLogDistributor(1024)
+	WebServer = NewWebServer()
+	rg := &run.Group{}
+	rg.Add(PMRun, PMStop)
+	rg.Add(WebServer.Run, WebServer.Stop)
+	rg.Add(LazyQuiter.Run, LazyQuiter.Stop)
+	err := rg.Run()
+	if err != nil {
+		Logger.LogPrint("main", hiedalog.DLN_FATAL, "error in running:", err)
+	}
+	os.Exit(0)
+}

+ 16 - 0
pkgtest/pmtest-server/pm_wrapper.go

@@ -0,0 +1,16 @@
+package main
+
+import "git.swzry.com/zry/GoHiedaLogger/hiedalog"
+
+func PMRun() error {
+	pmErrcCh := PM.Start()
+	err := <-pmErrcCh
+	return err
+}
+
+func PMStop(_ error) {
+	err := PM.Stop()
+	if err != nil {
+		Logger.LogPrint("main", hiedalog.DLN_ERROR, "error in stopping: ", err)
+	}
+}

+ 207 - 0
pkgtest/pmtest-server/web.go

@@ -0,0 +1,207 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"git.swzry.com/zry/GoHiedaLogger/hieda_ginutil"
+	"git.swzry.com/zry/GoHiedaLogger/hiedalog"
+	"git.swzry.com/zry/ran-proc/rpcore"
+	"github.com/gin-gonic/gin"
+	"github.com/hashicorp/go-uuid"
+	"github.com/liushuochen/gotable"
+	"net/http"
+	"os"
+	"time"
+)
+
+type WebServerClass struct {
+	engine     *gin.Engine
+	httpServer *http.Server
+	apiGroup   *gin.RouterGroup
+	wsGroup    *gin.RouterGroup
+}
+
+func NewWebServer() *WebServerClass {
+	web := &WebServerClass{
+		engine: gin.New(),
+	}
+	glf := hieda_ginutil.GinLoggerWithComplexLogger(hieda_ginutil.GinLoggerConfig{
+		Logger:       Logger,
+		ModuleName:   "web",
+		LevelMapFunc: hieda_ginutil.GetDefaultLevelMapFunc(),
+	})
+	web.engine.Use(glf, gin.Recovery())
+	web.httpServer = &http.Server{
+		Addr:    "0.0.0.0:9198",
+		Handler: web.engine,
+	}
+	return web
+}
+
+func (s *WebServerClass) Run() error {
+	s.defineRoutes()
+	return s.httpServer.ListenAndServe()
+}
+
+func (s *WebServerClass) Stop(_ error) {
+	ctx, cncl := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cncl()
+	err := s.httpServer.Shutdown(ctx)
+	if err != nil {
+		Logger.LogPrint("main", hiedalog.DLN_ERROR, "error in shutting down web server: ", err)
+	}
+}
+
+func (s *WebServerClass) defineRoutes() {
+	s.engine.GET("/", s.wh_ListAllStatus)
+	s.apiGroup = s.engine.Group("/api/")
+	s.wsGroup = s.engine.Group("/ws/")
+	s.wsGroup.GET("/get-cmd-output.satori", s.wh_ReadCmdOutput)
+	s.apiGroup.POST("/new-task.satori", s.wh_NewTask)
+}
+
+func (s *WebServerClass) wh_ListAllStatus(ctx *gin.Context) {
+	st, err := PM.ListAllStatus()
+	if err != nil {
+		ctx.String(500, "500 Internal Server Error")
+		Logger.LogPrint("web-api", hiedalog.DLN_WARN, "failed list status: ", err)
+		return
+	}
+	tb, err := gotable.Create("CPID", "Name", "Status", "PID", "Last Start Time")
+	if err != nil {
+		ctx.String(500, "500 Internal Server Error")
+		Logger.LogPrint("web-api", hiedalog.DLN_WARN, "failed create table: ", err)
+		return
+	}
+	for _, v := range st {
+		err = tb.AddRow(v.ToPrintableTableData())
+		if err != nil {
+			Logger.LogPrint("debug", hiedalog.DLN_INFO, "Failed Print Table:", err)
+		}
+	}
+	ctx.String(200, tb.String())
+}
+
+func (s *WebServerClass) ApiChecker(ctx *gin.Context, BindObj interface{}) bool {
+	key := ctx.GetHeader("AuthKey")
+	if key != AccessKey {
+		ctx.JSON(403, &gin.H{
+			"status": "403",
+			"errMsg": "auth failed",
+		})
+		Logger.LogPrintf("web-api", hiedalog.DLN_VERBOSE, "client %s auth failed", ctx.Request.RemoteAddr)
+		return false
+	}
+	err := ctx.BindJSON(BindObj)
+	if err != nil {
+		ctx.JSON(400, &gin.H{
+			"status": "400",
+			"errMsg": "bad request json format",
+		})
+		Logger.LogPrintf("web-api", hiedalog.DLN_VERBOSE, "client %s request json invalid: %v", ctx.Request.RemoteAddr, err)
+		return false
+	}
+	return true
+}
+
+func (s *WebServerClass) wh_ReadCmdOutput(ctx *gin.Context) {
+	key := ctx.Query("auth-key")
+	if key != AccessKey {
+		ctx.JSON(403, &gin.H{
+			"status": "403",
+			"errMsg": "auth failed",
+		})
+		Logger.LogPrintf("web-api", hiedalog.DLN_VERBOSE, "client %s auth failed", ctx.Request.RemoteAddr)
+		return
+	}
+	WsLogDistr.HandleNewConnections(ctx)
+}
+
+func (s *WebServerClass) wh_NewTask(ctx *gin.Context) {
+	var input ApiJsonDef_NewTask
+	if !s.ApiChecker(ctx, &input) {
+		return
+	}
+	taskName := input.TaskName
+	if taskName == "" {
+		ustr, _ := uuid.GenerateUUID()
+		taskName = fmt.Sprintf("task-%s", ustr)
+	}
+	cpid, err := PM.CreateTask(&rpcore.NewTaskConfig{
+		Name: taskName,
+		CmdInfo: &rpcore.CmdInfoClass{
+			Name:   input.ExecutablePath,
+			Args:   append([]string{input.ExecutablePath}, input.Args...),
+			Env:    os.Environ(),
+			Dir:    input.WorkDir,
+			Stdin:  nil,
+			Stdout: WsLogDistr,
+			Stderr: WsLogDistr,
+		},
+		DoneCallback: func(cpid int64, name string) {
+			Logger.LogPrintf("task-done", hiedalog.DLN_INFO, "task (cpid=%16X, name='%s') done.", cpid, name)
+		},
+		ExecTimeout:     time.Duration(input.ExecTimeout) * time.Millisecond,
+		ShutdownTimeout: 10 * time.Second,
+		ShutdownActor:   GracefulShutdownActorForThis,
+	})
+	if err != nil {
+		ctx.JSON(500, &gin.H{
+			"status": "500",
+			"errMsg": "failed create task: internal error",
+		})
+		Logger.LogPrint("web-api", hiedalog.DLN_WARN, "failed create task: ", err)
+		return
+	}
+	ctx.JSON(200, &gin.H{
+		"status":  "200",
+		"infoMsg": "task created",
+		"cpid":    fmt.Sprintf("%16X", cpid),
+		"name":    taskName,
+	})
+}
+
+func (s *WebServerClass) wh_NewDaemon(ctx *gin.Context) {
+	var input ApiJsonDef_NewDaemon
+	if !s.ApiChecker(ctx, &input) {
+		return
+	}
+	daemonName := input.DaemonName
+	if daemonName == "" {
+		ctx.JSON(500, &gin.H{
+			"status": "400",
+			"errMsg": "failed create daemon: daemonName should not be empty",
+		})
+		Logger.LogPrint("web-api", hiedalog.DLN_WARN, "failed create task: daemonName should not be empty")
+		return
+	}
+	cpid, err := PM.CreateDaemon(&rpcore.NewDaemonConfig{
+		Name: daemonName,
+		CmdInfo: &rpcore.CmdInfoClass{
+			Name:   input.ExecutablePath,
+			Args:   append([]string{input.ExecutablePath}, input.Args...),
+			Env:    os.Environ(),
+			Dir:    input.WorkDir,
+			Stdin:  nil,
+			Stdout: WsLogDistr,
+			Stderr: WsLogDistr,
+		},
+		ShutdownTimeout:   10 * time.Second,
+		ShutdownActor:     GracefulShutdownActorForThis,
+		EnableAfterCreate: input.EnableAfterCreate,
+	})
+	if err != nil {
+		ctx.JSON(500, &gin.H{
+			"status": "500",
+			"errMsg": "failed create daemon: internal error",
+		})
+		Logger.LogPrint("web-api", hiedalog.DLN_WARN, "failed create daemon: ", err)
+		return
+	}
+	ctx.JSON(200, &gin.H{
+		"status":  "200",
+		"infoMsg": "task created",
+		"cpid":    fmt.Sprintf("%16X", cpid),
+		"name":    daemonName,
+	})
+}

+ 11 - 0
pkgtest/run-group-test-2023-0125-0410/main.go

@@ -0,0 +1,11 @@
+package main
+
+import "fmt"
+
+func main() {
+	ui := NewUI()
+	err := ui.Run()
+	if err != nil {
+		fmt.Println("[UI] error: ", err)
+	}
+}

+ 52 - 0
pkgtest/run-group-test-2023-0125-0410/task.go

@@ -0,0 +1,52 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"math/rand"
+	"time"
+)
+
+type Task struct {
+	id     int
+	ctx    context.Context
+	cncl   context.CancelFunc
+	writer io.Writer
+}
+
+func NewTask(id int, ctx context.Context, writer io.Writer) *Task {
+	nctx, cncl := context.WithCancel(ctx)
+	t := &Task{
+		id:     id,
+		ctx:    nctx,
+		cncl:   cncl,
+		writer: writer,
+	}
+	return t
+}
+
+func (t *Task) Task() error {
+	tstr := time.Now().Format(time.RFC3339Nano)
+LabelLoop:
+	for {
+		_, _ = fmt.Fprintf(t.writer, "[Task %d] printing, time=%s.\n", t.id, tstr)
+		jitter := rand.Int31n(1000)
+		sltime := time.Duration(jitter+1000) * time.Millisecond
+		time.Sleep(sltime)
+		select {
+		case <-t.ctx.Done():
+			{
+				break LabelLoop
+			}
+		default:
+			continue LabelLoop
+		}
+	}
+	_, _ = fmt.Fprintf(t.writer, "[Task %d] printing task end.\n", t.id)
+	return nil
+}
+
+func (t *Task) Cancel(_ error) {
+	t.cncl()
+}

+ 130 - 0
pkgtest/run-group-test-2023-0125-0410/ui.go

@@ -0,0 +1,130 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/gdamore/tcell/v2"
+	"github.com/oklog/run"
+	"github.com/rivo/tview"
+	"io"
+)
+
+type UI struct {
+	app    *tview.Application
+	flex   *tview.Flex
+	logbox *tview.TextView
+	menu   *tview.List
+
+	running bool
+	grp     *run.Group
+	pctx    context.Context
+	cncl    context.CancelFunc
+	tasks   []*Task
+	logwr   io.Writer
+}
+
+func NewUI() *UI {
+	ui := &UI{
+		app:    tview.NewApplication(),
+		flex:   tview.NewFlex(),
+		logbox: tview.NewTextView(),
+		menu:   tview.NewList(),
+	}
+	ui.flex.AddItem(ui.menu, 32, 0, true)
+	ui.flex.AddItem(ui.logbox, 0, 1, false)
+	ui.flex.SetDirection(tview.FlexColumn)
+	ui.logbox.SetScrollable(true)
+	ui.logbox.SetBackgroundColor(tcell.ColorNavy)
+	ui.logwr = tview.ANSIWriter(ui.logbox)
+	ui.menu.AddItem("start", "", 0, ui.startTasks)
+	ui.menu.AddItem("cancel parent", "", 0, ui.killParent)
+	ui.menu.AddItem("cancel Task1", "", 0, func() {
+		ui.killTask(0)
+	})
+	ui.menu.AddItem("cancel Task2", "", 0, func() {
+		ui.killTask(1)
+	})
+	ui.menu.AddItem("cancel Task3", "", 0, func() {
+		ui.killTask(2)
+	})
+
+	ui.app.SetRoot(ui.flex, true)
+	ui.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+		switch event.Key() {
+		case tcell.KeyF1:
+			{
+				ui.app.SetFocus(ui.menu)
+				break
+			}
+		case tcell.KeyF2:
+			{
+				ui.app.SetFocus(ui.logbox)
+				break
+			}
+		default:
+			{
+				return event
+			}
+		}
+		return nil
+	})
+	return ui
+}
+
+func (u *UI) killParent() {
+	if u.running {
+		_, _ = fmt.Fprintln(u, "[UI] cancel parent")
+		u.cncl()
+	} else {
+		_, _ = fmt.Fprintln(u, "[UI] tasks not running")
+	}
+}
+
+func (u *UI) startTasks() {
+	if !u.running {
+		u.grp = &run.Group{}
+		ctx, cncl := context.WithCancel(context.Background())
+		u.pctx = ctx
+		u.cncl = cncl
+
+		u.tasks = make([]*Task, 3)
+		for i := 0; i < 3; i++ {
+			t := NewTask(i+1, u.pctx, u)
+			u.tasks[i] = t
+			u.grp.Add(t.Task, t.Cancel)
+		}
+		go func() {
+			_, _ = fmt.Fprintln(u, "[UI] run tasks.")
+			u.running = true
+			err := u.grp.Run()
+			if err != nil {
+				_, _ = fmt.Fprintln(u, "[UI] all tasks end, with error:", err)
+			} else {
+				_, _ = fmt.Fprintln(u, "[UI] all tasks end, with no error")
+			}
+			u.running = false
+		}()
+	} else {
+		_, _ = fmt.Fprintln(u, "[UI] tasks already running")
+	}
+}
+
+func (u *UI) killTask(id int) {
+	if !u.running {
+		_, _ = fmt.Fprintln(u, "[UI] tasks not running")
+		return
+	}
+	v := u.tasks[id]
+	if v != nil {
+		_, _ = fmt.Fprintf(u, "[UI] cancel task %d\n", id+1)
+		v.Cancel(nil)
+	}
+}
+
+func (u *UI) Write(p []byte) (n int, err error) {
+	return u.logwr.Write(p)
+}
+
+func (u *UI) Run() error {
+	return u.app.Run()
+}

+ 83 - 0
pkgtest/test-app-a/main.go

@@ -0,0 +1,83 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	lazyquiter "git.swzry.com/zry/go-lazy-quiter"
+	"math/rand"
+	"os"
+	"time"
+)
+
+var quitch chan int
+var timeout int
+var PID int
+var ExtraMsg string
+
+func main() {
+	flag.IntVar(&timeout, "t", 0, "Quit after specified milliseconds. 0 for unlimited.")
+	flag.StringVar(&ExtraMsg, "m", "nya~~~~~~~~~~~~~~~~", "The message will be printed repeatedly.")
+	flag.Parse()
+	PID = os.Getpid()
+	ctx, cncl := context.WithCancel(context.Background())
+	lq := lazyquiter.NewLazyQuiter()
+	quitch = make(chan int, 0)
+	abortch := make(chan int, 0)
+	go printing(ctx, quitch)
+	go func() {
+		lq.Wait()
+		abortch <- 0
+	}()
+	if timeout > 0 {
+		go func() {
+			select {
+			case <-time.After(time.Duration(timeout) * time.Millisecond):
+				{
+					AppPrint("execute ctx cancel by timeout.")
+					cncl()
+				}
+			case <-abortch:
+				{
+					AppPrint("execute ctx cancel by user abort.")
+					cncl()
+				}
+			}
+		}()
+	} else {
+		go func() {
+			<-abortch
+			AppPrint("execute ctx cancel by user abort.")
+			cncl()
+		}()
+	}
+	AppPrint("main thread wait for done.")
+	quitCode := <-quitch
+	AppPrint(fmt.Sprint("main thread end, quit code: ", quitCode))
+	os.Exit(quitCode)
+}
+
+func printing(ctx context.Context, okchan chan int) {
+LabelLoop:
+	for {
+		AppPrint(fmt.Sprint("msg print: ", ExtraMsg))
+		jitter := rand.Int31n(1000)
+		sltime := time.Duration(jitter+1000) * time.Millisecond
+		time.Sleep(sltime)
+		select {
+		case <-ctx.Done():
+			{
+				break LabelLoop
+			}
+		default:
+			continue LabelLoop
+		}
+	}
+	AppPrint("printing goroutine end.")
+	okchan <- 0
+}
+
+func AppPrint(text string) {
+	tstr := time.Now().Format(time.RFC3339Nano)
+	fmt.Printf("[test-app-a:%08d] <%s> %s\n", PID, tstr, text)
+}

+ 75 - 0
pkgtest/tomb-test-2023-0124-1301/main.go

@@ -0,0 +1,75 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	lazyquiter "git.swzry.com/zry/go-lazy-quiter"
+	tombv2 "gopkg.in/tomb.v2"
+	"math/rand"
+	"os"
+	"time"
+)
+
+func main() {
+	lq := lazyquiter.NewLazyQuiter()
+	quitch := make(chan error, 0)
+	tt := NewTask()
+	tt.Start()
+	go func() {
+		lq.Wait()
+		fmt.Println("[tomb-test-2023-0124-1301] stop task.")
+		err := tt.Stop()
+		quitch <- err
+	}()
+	fmt.Println("[tomb-test-2023-0124-1301] main thread wait for done.")
+	err := <-quitch
+	if err != nil {
+		fmt.Println("[tomb-test-2023-0124-1301] main thread end with error:", err)
+	} else {
+		fmt.Println("[tomb-test-2023-0124-1301] main thread end with no error.")
+	}
+}
+
+type TombedTask struct {
+	tomb *tombv2.Tomb
+	ctx  context.Context
+}
+
+func NewTask() *TombedTask {
+	tt := &TombedTask{}
+	t, ctx := tombv2.WithContext(context.Background())
+	tt.tomb = t
+	tt.ctx = ctx
+	return tt
+}
+
+func (t *TombedTask) task() error {
+	pid := os.Getpid()
+	tstr := time.Now().Format(time.RFC3339Nano)
+LabelLoop:
+	for {
+		fmt.Printf("[tomb-test-2023-0124-1301] pid=%d\ttime=%s\n", pid, tstr)
+		jitter := rand.Int31n(1000)
+		sltime := time.Duration(jitter+1000) * time.Millisecond
+		time.Sleep(sltime)
+		select {
+		case <-t.tomb.Dying():
+			{
+				break LabelLoop
+			}
+		default:
+			continue LabelLoop
+		}
+	}
+	fmt.Println("[tomb-test-2023-0124-1301] printing task end.")
+	return nil
+}
+
+func (t *TombedTask) Start() {
+	t.tomb.Go(t.task)
+}
+
+func (t *TombedTask) Stop() error {
+	t.tomb.Kill(nil)
+	return t.tomb.Wait()
+}

+ 50 - 0
pkgtest/ws-client-test/ws-client-test.go

@@ -0,0 +1,50 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"github.com/gorilla/websocket"
+	"os"
+	"os/signal"
+	"syscall"
+)
+
+var URL string
+
+func main() {
+	flag.StringVar(&URL, "u", "", "URL to connect")
+	flag.Parse()
+	c, _, err := websocket.DefaultDialer.Dial(URL, nil)
+	if err != nil {
+		fmt.Println("failed dial ws: ", err)
+		return
+	}
+	chSIGINT := make(chan os.Signal, 1)
+	chError := make(chan error, 1)
+	signal.Notify(chSIGINT, syscall.SIGINT)
+	go func() {
+		for {
+			_, dat, err := c.ReadMessage()
+			if err != nil {
+				chError <- fmt.Errorf("error reading websocket message: %s", err.Error())
+				return
+			}
+			fmt.Print(string(dat))
+		}
+	}()
+	select {
+	case <-chSIGINT:
+		fmt.Println()
+		fmt.Println("gotun-rlog abort by user Ctrl+C.")
+		err := c.Close()
+		if err != nil {
+			fmt.Println("failed close websocket connection:", err.Error())
+		}
+		return
+	case err := <-chError:
+		if err != nil {
+			fmt.Println("websocket error: ", err.Error())
+		}
+		return
+	}
+}