[pah] rewrite systray support

This commit is contained in:
Simon Lindholm
2021-05-04 13:27:42 +02:00
parent f177b59167
commit 1c07a7ee17
16 changed files with 720 additions and 138 deletions
+45
View File
@@ -0,0 +1,45 @@
name: Systray
on:
push:
branches: [ main ]
paths:
- 'src/net/cmd/systray/*'
pull_request:
branches: [ main ]
paths:
- 'src/net/cmd/systray/*'
jobs:
build:
name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-16.04
binary: permuter-systray-linux
- os: windows-latest
binary: permuter-systray.exe
- os: macos-latest
binary: permuter-systray-macos
steps:
- uses: actions/checkout@main
- name: Install gtk3
if: ${{ matrix.os == 'ubuntu-16.04' }}
run: sudo apt-get install libgtk-3-dev libappindicator3-dev
- name: Setup Go environment
uses: actions/setup-go@v2.1.3
- name: Build
run: go build -o ${{ matrix.binary }} -ldflags "-s -w" tray.go
working-directory: src/net/cmd/systray/
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: ${{ matrix.binary }}
path: src/net/cmd/systray/${{ matrix.binary }}
-1
View File
@@ -70,7 +70,6 @@ To allow others to use your computer for permuter runs, do the following:
- install Docker (used for sandboxing and to ensure a consistent environment)
- if on Linux, add yourself to the Docker group: `sudo usermod -aG docker $USER`
- install required packages: `python3 -m pip install docker`
(optionally add `pystray` and `Pillow` for experimental systray support)
- open a terminal, and run `./pah.py run-server` to start the server.
There are a few required arguments (e.g. how many cores to use), see `--help` for more details.
+1 -2
View File
@@ -264,8 +264,7 @@ def start_client(
feedback_queue,
)
thread = threading.Thread(target=conn.run)
thread.daemon = True
thread = threading.Thread(target=conn.run, daemon=True)
thread.start()
stats = (num_clients, num_servers, num_cores)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

+310 -111
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
permuter-systray
permuter-systray.exe
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Zack Young
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+13
View File
@@ -0,0 +1,13 @@
# systray
This directory contains a Go application that shows a system tray, which the Python code interacts with.
It is a fork of https://github.com/felixhao28/systray-portable.
To build it:
- install Go
- if on Linux, install dependencies: `libgtk-3-dev`, `libappindicator3-dev`
- run `go build`
If on Windows, this needs to be done *outside* of WSL.
+7
View File
@@ -0,0 +1,7 @@
module permuter-systray
go 1.15
require github.com/getlantern/systray v1.1.0
replace github.com/getlantern/systray v1.1.0 => github.com/simonlindholm/systray v1.1.1-0.20210502122945-b7c77212cd56
+4
View File
@@ -0,0 +1,4 @@
github.com/simonlindholm/systray v1.1.1-0.20210502122945-b7c77212cd56 h1:UZcM1HdV25CQhhJD340jxRLRGl0V11V0wIoUDKTOZMI=
github.com/simonlindholm/systray v1.1.1-0.20210502122945-b7c77212cd56/go.mod h1:N5dpnnWiJhCxh+gXuNgDS2p5MjgcVR/TGwWuaDc4gLk=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+287
View File
@@ -0,0 +1,287 @@
package main
import (
"bufio"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/signal"
"reflect"
"strings"
"syscall"
"github.com/getlantern/systray"
)
func main() {
systray.Run(onReady, onExit)
}
func onExit() {
os.Exit(0)
}
// Item represents an item in the menu
type Item struct {
Icon string `json:"icon"`
Title string `json:"title"`
Tooltip string `json:"tooltip"`
Enabled bool `json:"enabled"`
Checked bool `json:"checked"`
Hidden bool `json:"hidden"`
Items []Item `json:"items"`
InternalID int `json:"__id"`
}
// Menu has an icon, title and list of items
type Menu struct {
Icon string `json:"icon"`
Title string `json:"title"`
Tooltip string `json:"tooltip"`
Items []Item `json:"items"`
}
// Action for an item?..
type Action struct {
Type string `json:"type"`
Item Item `json:"item"`
Menu Menu `json:"menu"`
}
// ClickEvent for an click event
type ClickEvent struct {
Type string `json:"type"`
InternalID int `json:"__id"`
}
func readJSON(reader *bufio.Reader, v interface{}) error {
input, err := reader.ReadString('\n')
if err != nil {
return err
}
if len(input) < 1 {
return fmt.Errorf("Empty line")
}
lineReader := strings.NewReader(input[0 : len(input)-1])
if err := json.NewDecoder(lineReader).Decode(v); err != nil {
return err
}
return nil
}
func addMenuItem(items *[]*systray.MenuItem, seqID2InternalID *[]int, internalID2SeqID *map[int]int, item *Item, parent *systray.MenuItem) {
if item.Title == "<SEPARATOR>" {
systray.AddSeparator()
*items = append(*items, nil)
} else {
var menuItem *systray.MenuItem
if parent == nil {
menuItem = systray.AddMenuItem(item.Title, item.Tooltip)
} else {
menuItem = parent.AddSubMenuItem(item.Title, item.Tooltip)
}
if item.Checked {
menuItem.Check()
} else {
menuItem.Uncheck()
}
if item.Enabled {
menuItem.Enable()
} else {
menuItem.Disable()
}
if len(item.Icon) > 0 {
icon, err := base64.StdEncoding.DecodeString(item.Icon)
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
menuItem.SetIcon(icon)
}
}
for i := 0; i < len(item.Items); i++ {
subitem := item.Items[i]
addMenuItem(items, seqID2InternalID, internalID2SeqID, &subitem, menuItem)
}
if item.Hidden {
menuItem.Hide()
}
*items = append(*items, menuItem)
}
seqID := len(*items) - 1
(*internalID2SeqID)[item.InternalID] = seqID
*seqID2InternalID = append(*seqID2InternalID, item.InternalID)
}
func onReady() {
signalChannel := make(chan os.Signal, 2)
signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
go func() {
for sig := range signalChannel {
switch sig {
case os.Interrupt, syscall.SIGTERM:
// handle SIGINT, SIGTERM
fmt.Fprintln(os.Stderr, "Quit")
systray.Quit()
default:
fmt.Fprintln(os.Stderr, "Unhandled signal:", sig)
}
}
}()
items := make([]*systray.MenuItem, 0)
seqID2InternalID := make([]int, 0)
internalID2SeqID := make(map[int]int)
fmt.Println(`{"type": "ready"}`)
reader := bufio.NewReader(os.Stdin)
var menu Menu
if err := readJSON(reader, &menu); err != nil {
fmt.Fprintln(os.Stderr, err)
systray.Quit()
return
}
icon, err := base64.StdEncoding.DecodeString(menu.Icon)
if err != nil {
fmt.Fprintln(os.Stderr, err)
systray.Quit()
return
}
systray.SetIcon(icon)
systray.SetTitle(menu.Title)
systray.SetTooltip(menu.Tooltip)
updateItem := func(action Action) {
item := action.Item
seqID := internalID2SeqID[action.Item.InternalID]
menuItem := items[seqID]
if menuItem == nil {
return
}
if item.Hidden {
menuItem.Hide()
} else {
if item.Checked {
menuItem.Check()
} else {
menuItem.Uncheck()
}
if item.Enabled {
menuItem.Enable()
} else {
menuItem.Disable()
}
menuItem.SetTitle(item.Title)
menuItem.SetTooltip(item.Tooltip)
if len(item.Icon) > 0 {
icon, err := base64.StdEncoding.DecodeString(item.Icon)
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
menuItem.SetIcon(icon)
}
}
menuItem.Show()
for _, child := range item.Items {
seqID = internalID2SeqID[child.InternalID]
items[seqID].Show()
}
}
}
updateMenu := func(action Action) {
m := action.Menu
if menu.Title != m.Title {
menu.Title = m.Title
systray.SetTitle(menu.Title)
}
if menu.Icon != m.Icon && m.Icon != "" {
menu.Icon = m.Icon
icon, err := base64.StdEncoding.DecodeString(menu.Icon)
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
systray.SetIcon(icon)
}
}
if menu.Tooltip != m.Tooltip {
menu.Tooltip = m.Tooltip
systray.SetTooltip(menu.Tooltip)
}
}
update := func(action Action) {
switch action.Type {
case "update-item":
updateItem(action)
case "update-menu":
updateMenu(action)
case "update-item-and-menu":
updateItem(action)
updateMenu(action)
case "exit":
systray.Quit()
}
}
for i := 0; i < len(menu.Items); i++ {
item := menu.Items[i]
addMenuItem(&items, &seqID2InternalID, &internalID2SeqID, &item, nil)
}
go func(reader *bufio.Reader) {
for {
var action Action
if err := readJSON(reader, &action); err != nil {
fmt.Fprintln(os.Stderr, err)
systray.Quit()
break
}
update(action)
}
}(reader)
stdoutEnc := json.NewEncoder(os.Stdout)
for {
itemsCnt := 0
for _, ch := range items {
if ch != nil {
itemsCnt++
}
}
cases := make([]reflect.SelectCase, itemsCnt)
caseCnt2SeqID := make([]int, len(items))
itemsCnt = 0
for i, ch := range items {
if ch == nil {
continue
}
cases[itemsCnt] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch.ClickedCh)}
caseCnt2SeqID[itemsCnt] = i
itemsCnt++
}
remaining := len(cases)
for remaining > 0 {
chosen, _, ok := reflect.Select(cases)
if !ok {
// The chosen channel has been closed, so zero out the channel to disable the case
cases[chosen].Chan = reflect.ValueOf(nil)
remaining--
continue
}
seqID := caseCnt2SeqID[chosen]
err := stdoutEnc.Encode(ClickEvent{
Type: "clicked",
InternalID: seqID2InternalID[seqID],
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
}
}
+4 -3
View File
@@ -291,13 +291,14 @@ def main() -> None:
p = Process(
target=multiprocess_worker,
args=(worker_queue, local_queue, task_queue),
daemon=True,
)
p.daemon = True
p.start()
local_queues.append(local_queue)
reader_thread = threading.Thread(target=read_loop, args=(task_queue, port))
reader_thread.daemon = True
reader_thread = threading.Thread(
target=read_loop, args=(task_queue, port), daemon=True
)
reader_thread.start()
remaining_work: Counter[str] = Counter()
+26 -21
View File
@@ -19,8 +19,10 @@ import nacl.utils
from ..helpers import exception_to_string, static_assert_unreachable
from .core import (
CancelToken,
Config,
PermuterData,
Port,
ServerError,
SocketPort,
connect,
file_read_fixed,
@@ -102,6 +104,7 @@ class NeedMoreWork:
@dataclass
class NetThreadDisconnected:
graceful: bool
message: Optional[str] = None
class Heartbeat:
@@ -195,6 +198,7 @@ class IoUserRemovePermuter:
@dataclass
class IoServerFailed:
graceful: bool
message: Optional[str]
class IoReconnect:
@@ -205,10 +209,6 @@ class IoShutdown:
pass
class IoWillSleep:
pass
@dataclass
class IoWorkDone:
score: Optional[int]
@@ -219,7 +219,7 @@ PermuterHandle = Tuple[int, CancelToken]
IoMessage = Union[
IoConnect, IoDisconnect, IoImmediateDisconnect, IoUserRemovePermuter, IoWorkDone
]
IoGlobalMessage = Union[IoReconnect, IoShutdown, IoServerFailed, IoWillSleep]
IoGlobalMessage = Union[IoReconnect, IoShutdown, IoServerFailed]
IoActivity = Tuple[
Optional[CancelToken], Union[Tuple[PermuterHandle, IoMessage], IoGlobalMessage]
]
@@ -250,12 +250,10 @@ class NetThread:
self._controller_queue = queue.Queue()
self._next_work_id = 0
self._read_thread = threading.Thread(target=self.read_loop)
self._read_thread.daemon = True
self._read_thread = threading.Thread(target=self.read_loop, daemon=True)
self._read_thread.start()
self._write_thread = threading.Thread(target=self.write_loop)
self._write_thread.daemon = True
self._write_thread = threading.Thread(target=self.write_loop, daemon=True)
self._write_thread.start()
def stop(self) -> None:
@@ -336,6 +334,10 @@ class NetThread:
self._main_queue.put(msg)
except EOFError:
self._main_queue.put(NetThreadDisconnected(graceful=True))
except ServerError as e:
self._main_queue.put(
NetThreadDisconnected(graceful=False, message=e.message)
)
except Exception:
traceback.print_exc()
self._main_queue.put(NetThreadDisconnected(graceful=False))
@@ -452,19 +454,20 @@ class ServerInner:
self._last_heartbeat = time.time()
self._last_heartbeat_lock = threading.Lock()
self._heartbeat_stop = threading.Event()
self._heartbeat_thread = threading.Thread(target=self._heartbeat_loop)
self._heartbeat_thread.daemon = True
self._heartbeat_thread = threading.Thread(
target=self._heartbeat_loop, daemon=True
)
self._heartbeat_thread.start()
# Start a thread for reading evaluator results and sending them on to
# the main loop queue.
self._read_eval_thread = threading.Thread(target=self._read_eval_loop)
self._read_eval_thread.daemon = True
self._read_eval_thread = threading.Thread(
target=self._read_eval_loop, daemon=True
)
self._read_eval_thread.start()
# Start a thread for the main loop.
self._main_thread = threading.Thread(target=self._main_loop)
self._main_thread.daemon = True
self._main_thread = threading.Thread(target=self._main_loop, daemon=True)
self._main_thread.start()
def _send_controller(self, msg: Output) -> None:
@@ -590,7 +593,7 @@ class ServerInner:
self._need_work()
elif isinstance(msg, NetThreadDisconnected):
self._send_io_global(IoServerFailed(msg.graceful))
self._send_io_global(IoServerFailed(msg.graceful, msg.message))
else:
static_assert_unreachable(msg)
@@ -674,9 +677,6 @@ class ServerInner:
self._handle_message(msg)
if not self._active and self._main_queue.empty():
self._send_io_global(IoWillSleep())
def _heartbeat_loop(self) -> None:
second_attempt = False
while True:
@@ -894,19 +894,24 @@ class Server:
_server: Optional[ServerInner]
_options: ServerOptions
_config: Config
_io_queue: "queue.Queue[IoActivity]"
def __init__(
self, options: ServerOptions, io_queue: "queue.Queue[IoActivity]"
self,
options: ServerOptions,
config: Config,
io_queue: "queue.Queue[IoActivity]",
) -> None:
self._server = None
self._options = options
self._config = config
self._io_queue = io_queue
def start(self) -> None:
assert self._server is None
net_port = connect()
net_port = connect(self._config)
net_port.send_json(
{
"method": "connect_server",