mirror of
https://github.com/encounter/decomp-permuter.git
synced 2026-07-10 03:18:46 -07:00
[pah] rewrite systray support
This commit is contained in:
@@ -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 }}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
permuter-systray
|
||||
permuter-systray.exe
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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=
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user