onemillioncheckboxes/swarm/manager.py

168 lines
5.2 KiB
Python
Raw Normal View History

from multiprocessing.shared_memory import SharedMemory
from typing import Optional
import asyncio
import socketio
import aiohttp
from aiohttp_socks import ProxyConnector
from PIL import (
Image,
ImageFont,
ImageDraw,
ImageFilter,
ImageSequence,
ImageChops,
)
from enum import IntFlag
from base64 import b64decode
import signal
import os
import time
class PixelMask(IntFlag):
AVOID = 16
MASK = 32
FILL = 64
CHECKED = 128
class Manager:
def __init__(self):
self.shmem: Optional[SharedMemory] = None
self.shmem_name = "omcb-bot"
self.base = "https://onemillioncheckboxes.com"
self.last_update = 0
self.bits_toggled_on = 0
self.bits_toggled_off = 0
self.last_printout = 0
async def listener(self):
sio = socketio.AsyncClient()
sio.on("connect", self.on_connect)
sio.on("batched_bit_toggles", self.on_batched_bit_toggles)
sio.on("full_state", self.on_full_state)
await sio.connect(f"{self.base}")
await sio.wait()
def update_shmem(self, state: bytes):
if not self.shmem:
raise ValueError("shared memory is not initialized yet")
buffer = bytearray(bytes(1000000))
for i in range(1000000):
byte, bit = divmod(i, 8)
if state[byte] & (0x80 >> bit):
buffer[i] |= PixelMask.CHECKED
else:
buffer[i] &= ~PixelMask.CHECKED
self.shmem.buf[:] = buffer
async def on_connect(self):
async with aiohttp.ClientSession() as http:
async with http.get(f"{self.base}/api/initial-state") as req:
data = await req.json()
buffer = b64decode(data["full_state"].encode() + b"=")
self.update_shmem(buffer)
self.last_update = data["timestamp"]
async def on_full_state(self, data):
if not self.shmem:
raise ValueError("shared memory is not initialized yet")
buffer = b64decode(data["full_state"].encode() + b"=")
self.update_shmem(buffer)
self.last_update = data["timestamp"]
async def on_batched_bit_toggles(self, data):
if not self.shmem:
raise ValueError("shared memory is not initialized yet")
bits_on, bits_off, timestamp = data
if timestamp < self.last_update:
print("old update, ignoring")
self.last_update = timestamp
self.bits_toggled_on += len(bits_on)
self.bits_toggled_off = len(bits_off)
for bit in bits_on:
self.shmem.buf[bit] |= PixelMask.CHECKED
for bit in bits_off:
self.shmem.buf[bit] &= ~PixelMask.CHECKED
since_last_printout = time.time() - self.last_printout
if since_last_printout >= 5:
self.last_printout = time.time()
print()
print(f"Toggled on: {self.bits_toggled_on / since_last_printout}/s")
print(f"Toggled off: {self.bits_toggled_off / since_last_printout}/s")
self.bits_toggled_on = self.bits_toggled_off = 0
def on_sigusr1(self, signum, frame):
if not self.shmem:
raise ValueError("shared memory is not initialized yet")
print("Caught SIGUSR1, dumping state")
buf = bytes(self.shmem.buf[:])
with Image.new("RGB", (1000, 1000), 0) as im:
for i in range(1000000):
y, x = divmod(i, 1000)
im.putpixel((x, y), (
255 if buf[i] & PixelMask.FILL else 0,
255 if buf[i] & PixelMask.MASK else 0,
255 if buf[i] & PixelMask.CHECKED else 0
))
im.save("state.png")
async def animator(self):
while True:
await asyncio.sleep(0.1)
def add_avoid_range(self, rng: range):
if not self.shmem:
raise ValueError("shared memory is not initialized yet")
for ndx in rng:
self.shmem.buf[ndx] |= PixelMask.AVOID
def add_avoid_rect(self, sx: int, sy: int, w: int, h: int):
for y in range(sy, sy + h):
ox = y * 1000
self.add_avoid_range(range(sx + ox, sx + w + ox))
def add_avoid_index(self, index: int):
if not self.shmem:
raise ValueError("shared memory is not initialized yet")
assert 0 <= index < 1000000
self.shmem.buf[index] |= PixelMask.AVOID
def add_avoid_image(self, im: Image.Image):
assert im.width == 1000
assert im.height == 1000
for i in range(1000000):
y, x = divmod(i, 1000)
la = im.getpixel((x, y))
assert isinstance(la, (tuple, list)) and len(la) == 2
if la[1]:
self.add_avoid_index(i)
async def __aenter__(self):
self.shmem = SharedMemory(self.shmem_name, True, 1000000)
return self
async def __aexit__(self, a, b, c):
if self.shmem:
print("cleaning up shmem")
self.shmem.close()
self.shmem.unlink()
async def main():
print(f"PID: {os.getpid()}")
async with Manager() as mgr:
signal.signal(signal.SIGUSR1, mgr.on_sigusr1)
await mgr.listener()
if __name__ == "__main__":
asyncio.run(main())