Skip to content

Commit

Permalink
Merge pull request #111 from spyoungtech/daemon
Browse files Browse the repository at this point in the history
Daemon mode
  • Loading branch information
spyoungtech committed Oct 13, 2021
2 parents 137670a + 105bacd commit 197bb9e
Show file tree
Hide file tree
Showing 63 changed files with 1,972 additions and 78 deletions.
251 changes: 251 additions & 0 deletions ahk/daemon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import os
import asyncio
import warnings
from ahk.autohotkey import AsyncAHK, AHK
import subprocess
import threading
import queue
import atexit

def escape(s):
s = s.replace('\n', '`n')
return s

class STOP:
"""A sentinel value"""
...

class AHKDaemon(AHK):
proc: subprocess.Popen
_template_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'templates')
_template = os.path.join(_template_path, 'daemon.ahk')
_template_overrides = os.listdir(f'{_template_path}/daemon')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.queue = queue.Queue()
self.result_queue = queue.Queue()
self.proc: asyncio.subprocess.Process
self.proc = None
self.thread = None
self._is_running = False
self.run_lock = threading.Lock()
template = self.env.get_template('_daemon.ahk')
with open(self._template, 'w') as f:
f.write(template.render())

def _run(self):
if self._is_running:
raise RuntimeError("Already running")
self._is_running = True
runargs = [self.executable_path, self._template]
proc = subprocess.Popen(runargs,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
self.proc = proc
atexit.register(self.proc.terminate)

def worker(self):
while True:
command = self.queue.get()
if command is STOP:
break
self.proc.stdin.write(command + b'\n')
self.proc.stdin.flush()
num_lines = int(self.proc.stdout.readline().strip())
res = b''.join(self.proc.stdout.readline() for _ in range(num_lines + 1))
self.result_queue.put_nowait(res[:-1])
self.queue.task_done()

def _start(self):
try:
self.thread = threading.Thread(target=self.worker, daemon=True)
self.thread.start()
yield
finally:
if self.proc is not None:
self.proc.kill()

def stop(self):
if self._is_running:
if hasattr(self, '_gen') and self._gen is not None:
try:
next(self._gen)
except StopIteration:
pass
self.queue.put_nowait(STOP)
if self.thread is not None:
self.thread.join()
self._is_running = False

def start(self):
self._gen = self._start()
self._gen.send(None)
self._run()

def render_template(self, template_name, directives=None, blocking=True, **kwargs):
name = template_name.split('/')[-1]
if name in self._template_overrides:
template_name = f'daemon/{name}'
blocking = False
directives = None
kwargs['_daemon'] = True
return super().render_template(template_name, directives=directives, blocking=blocking, **kwargs)

def run_script(self, script_text: str, decode=True, blocking=True, **runkwargs):
if not blocking:
warnings.warn("blocking=False in daemon mode is not supported", stacklevel=3)
if not self._is_running:
raise RuntimeError("Not running! Must call .start() first!")
script_text = script_text.replace('#NoEnv', '', 1)
with self.run_lock:
for line in script_text.split('\n'):
line = line.strip()
if not line or line.startswith("FileAppend"):
continue
self.queue.put_nowait(line.encode('utf-8'))
self.queue.join()
res = []
while not self.result_queue.empty():
res.append(self.result_queue.get_nowait())
res = b'\n'.join(i for i in res if i)
if decode:
return res.decode('utf-8')
return res

def type(self, s, *args, **kwargs):
kwargs['raw'] = True
s = escape(s)
self.send(s, *args, **kwargs)

def show_tooltip(self, text: str, second=None, x="", y="", id="", blocking=True):
return super().show_tooltip(text, second=second, x=x, y=y, id=id, blocking=blocking)

def hide_tooltip(self, id):
return super().show_tooltip(text='', second='', x='', y='', id=id)

def hide_traytip(self):
self.run_script("HideTrayTip")

@staticmethod
def escape_sequence_replace(s):
s = escape(s)
return s


class AsyncAHKDaemon(AsyncAHK):
proc: asyncio.subprocess.Process
_template_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'templates')
_template = os.path.join(_template_path, 'daemon.ahk')
_template_overrides = os.listdir(f'{_template_path}/daemon')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.queue = asyncio.Queue()
self.result_queue = asyncio.Queue()
self.proc: asyncio.subprocess.Process
self.proc = None
self._is_running = False
self.run_lock = asyncio.Lock()
template = self.env.get_template('_daemon.ahk')
with open(self._template, 'w') as f:
f.write(template.render())

async def _run(self):
if self._is_running:
raise RuntimeError("Already running")
self._is_running = True
runargs = [self.executable_path, self._template]
proc = await asyncio.subprocess.create_subprocess_exec(*runargs,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
self.proc = proc

async def _get_command(self):
return await self.queue.get()

async def worker(self):
if not self._is_running:
await self.start()
while True:
command = await self.queue.get()
self.proc.stdin.write(command + b'\n')
await self.proc.stdin.drain()
num_lines = int(await self.proc.stdout.readline())
lines = []
for _ in range(num_lines + 1):
line = await self.proc.stdout.readline()
lines.append(line)
res = b''.join(lines)
self.result_queue.put_nowait(res[:-1])
self.queue.task_done()

def _start(self):
try:
asyncio.create_task(self.worker())
yield
finally:
if self.proc is not None:
self.proc.kill()

def stop(self):
if hasattr(self, '_gen') and self._gen is not None:
try:
next(self._gen)
except StopIteration:
pass
self._is_running = False

async def start(self):
self._gen = self._start()
self._gen.send(None)
await self._run()

def render_template(self, template_name, directives=None, blocking=True, **kwargs):
name = template_name.split('/')[-1]
if name in self._template_overrides:
template_name = f'daemon/{name}'
blocking = False
directives = None
kwargs['_daemon'] = True
return super().render_template(template_name, directives=directives, blocking=blocking, **kwargs)

async def a_run_script(self, script_text: str, decode=True, blocking=True, **runkwargs):
if not self._is_running:
raise RuntimeError("Not running! Must await .start() first!")
script_text = script_text.replace('#NoEnv', '', 1)
async with self.run_lock:
for line in script_text.split('\n'):
line = line.strip()
if not line or line.startswith("FileAppend"):
continue
self.queue.put_nowait(line.encode('utf-8'))
await self.queue.join()
res = []
while not self.result_queue.empty():
res.append(self.result_queue.get_nowait())
res = b'\n'.join(i for i in res if i)
if decode:
return res.decode('utf-8')
return res

async def type(self, s, *args, **kwargs):
kwargs['raw'] = True
s = escape(s)
await self.send(s, *args, **kwargs)

def show_tooltip(self, text: str, second=None, x="", y="", id="", blocking=True):
return super().show_tooltip(text, second=second, x=x, y=y, id=id, blocking=blocking)

def hide_tooltip(self, id):
return super().show_tooltip(text='', second='', x='', y='', id=id)

async def hide_traytip(self):
await self.run_script("HideTrayTip")

@staticmethod
def escape_sequence_replace(s):
s = escape(s)
return s

run_script = a_run_script
3 changes: 1 addition & 2 deletions ahk/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import warnings

from ahk.script import ScriptEngine, AsyncScriptEngine
from ahk.utils import escape_sequence_replace
from ahk.keys import Key
from ahk.directives import InstallKeybdHook, InstallMouseHook

Expand Down Expand Up @@ -141,7 +140,7 @@ def type(self, s, blocking=True):
:param s: the string to type
:param blocking: if ``True``, waits until script finishes, else returns immediately.
"""
s = escape_sequence_replace(s)
s = self.escape_sequence_replace(s)
return self.send_input(s, blocking=blocking) or None

def _send(self, s, raw=False, delay=None, blocking=True):
Expand Down
4 changes: 2 additions & 2 deletions ahk/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def _image_search(
if lower_bound:
x2, y2 = lower_bound
else:
x2, y2 = ('%A_ScreenWidth%', '%A_ScreenHeight%')
x2, y2 = ('A_ScreenWidth', 'A_ScreenHeight')
script = self.render_template(
'screen/image_search.ahk',
x1=x1,
Expand Down Expand Up @@ -162,7 +162,7 @@ def _pixel_search(
if lower_bound:
x2, y2 = lower_bound
else:
x2, y2 = ('%A_ScreenWidth%', '%A_ScreenHeight%')
x2, y2 = ('A_ScreenWidth', 'A_ScreenHeight')

script = self.render_template(
'screen/pixel_search.ahk',
Expand Down
16 changes: 12 additions & 4 deletions ahk/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import subprocess
import warnings
from shutil import which
from ahk.utils import make_logger
from ahk.utils import make_logger, escape_sequence_replace
from ahk.directives import Persistent
from jinja2 import Environment, FileSystemLoader
from typing import Set
Expand All @@ -32,8 +32,11 @@ class ExecutableNotFoundError(EnvironmentError):

def _resolve_executable_path(executable_path: str = ''):
if not executable_path:
executable_path = os.environ.get('AHK_PATH') or which(
'AutoHotkey.exe') or which('AutoHotkeyA32.exe')
executable_path = os.environ.get('AHK_PATH') \
or which('AutoHotkey.exe') \
or which('AutoHotkeyU64.exe') \
or which('AutoHotkeyU32.exe') \
or which('AutoHotkeyA32.exe')

if not executable_path:
if os.path.exists(DEFAULT_EXECUTABLE_PATH):
Expand All @@ -43,7 +46,8 @@ def _resolve_executable_path(executable_path: str = ''):
raise ExecutableNotFoundError(
'Could not find AutoHotkey.exe on PATH. '
'Provide the absolute path with the `executable_path` keyword argument '
'or in the AHK_PATH environment variable.'
'or in the AHK_PATH environment variable. '
'You may be able to resolve this error by installing the binary extra: pip install "ahk[binary]"'
)

if not os.path.exists(executable_path):
Expand Down Expand Up @@ -90,6 +94,10 @@ def __init__(self, executable_path: str = "", directives: Set = None, **kwargs):
directives = set()
self._directives = set(directives)

@staticmethod
def escape_sequence_replace(*args, **kwargs):
return escape_sequence_replace(*args, **kwargs)

def render_template(self, template_name, directives=None, blocking=True, **kwargs):
"""
Renders a given jinja template and returns a string of script text
Expand Down
Loading

0 comments on commit 197bb9e

Please sign in to comment.