Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialize the agent #34

Merged
merged 12 commits into from
Jan 5, 2018
Empty file added agent/__init__.py
Empty file.
35 changes: 35 additions & 0 deletions agent/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import signal
import logging
import threading
from tandem.executables.agent import TandemAgent

should_shutdown = threading.Event()


def signal_handler(signal, frame):
global should_shutdown
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we use global here? Wouldn't it be already declared in line 6?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that originally too, but it seems like you need to explicitly declare that you're referencing a global variable in Python (I think when making some mutating call). It didn't work for me without the global declaration.

https://stackoverflow.com/questions/423379/using-global-variables-in-a-function-other-than-the-one-that-created-them

should_shutdown.set()


def set_up_logging():
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M",
filename="/tmp/tandem-agent.log",
filemode="w",
)


def main():
set_up_logging()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

# Run the agent until asked to terminate
with TandemAgent() as agent:
should_shutdown.wait()


if __name__ == "__main__":
main()
Empty file added agent/tandem/__init__.py
Empty file.
Empty file.
31 changes: 31 additions & 0 deletions agent/tandem/executables/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging
from tandem.io.std_streams import StdStreams
from tandem.protocol.editor.handler import EditorProtocolHandler
from concurrent.futures import ThreadPoolExecutor


class TandemAgent:
def __init__(self):
self._std_streams = StdStreams(self._on_std_input)
self._editor_protocol = EditorProtocolHandler(self._std_streams)
self._main_executor = ThreadPoolExecutor(max_workers=1)

def __enter__(self):
self.start()
return self

def __exit__(self, exc_type, exc_value, traceback):
self.stop()

def start(self):
self._std_streams.start()
logging.info("Tandem Agent has started")

def stop(self):
self._std_streams.stop()
self._main_executor.shutdown()
logging.info("Tandem Agent has shut down")

def _on_std_input(self, data):
# Called by _std_streams after receiving a new message from the plugin
self._main_executor.submit(self._editor_protocol.handle_message, data)
Empty file added agent/tandem/io/__init__.py
Empty file.
31 changes: 31 additions & 0 deletions agent/tandem/io/std_streams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import sys
import logging
from threading import Thread


class StdStreams:
def __init__(self, handler_function):
self._handler_function = handler_function
self._reader = self._get_read_thread()

def start(self):
self._reader.start()

def stop(self):
self._reader.join()
sys.stdout.close()

def write(self, data):
sys.stdout.write(data)
sys.stdout.write("\n")
sys.stdout.flush()

def _get_read_thread(self):
def stdin_read():
try:
for line in sys.stdin:
self._handler_function(line)
except:
logging.exception("Exception when reading from stdin:")
raise
return Thread(target=stdin_read)
Empty file.
Empty file.
19 changes: 19 additions & 0 deletions agent/tandem/protocol/editor/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import logging
import tandem.protocol.editor.messages as m


class EditorProtocolHandler:
def __init__(self, std_streams):
self._std_streams = std_streams

def handle_message(self, data):
try:
message = m.deserialize(data)
response = m.serialize(message)
self._std_streams.write(response)
except m.EditorProtocolMarshalError:
pass
except:
logging.exception(
"Exception when handling editor protocol message:")
raise
77 changes: 77 additions & 0 deletions agent/tandem/protocol/editor/messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're going to want to split this into multiple files. In the future, it should be okay like this for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense. Since this defines messages between the agent and editor plugin, we may even want to consider using something like Protobuf/Thrift to define the messages so that they can easily be generated for different languages.

import enum


class EditorProtocolMarshalError:
pass


class EditorProtocolMessageType(enum.Enum):
UserChangedEditorText = "user-changed-editor-text"
ApplyText = "apply-text"


class UserChangedEditorText:
"""
Sent by the editor plugin to the agent to
notify it that the user changed the text buffer.
"""
def __init__(self, contents):
self.type = EditorProtocolMessageType.UserChangedEditorText
self.contents = contents

def to_payload(self):
return {
"contents": self.contents,
}

@staticmethod
def from_payload(payload):
return UserChangedEditorText(payload["contents"])


class ApplyText:
"""
Sent by the agent to the editor plugin to
notify it that someone else edited the text buffer.
"""
def __init__(self, contents):
self.type = EditorProtocolMessageType.ApplyText
self.contents = contents
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am changing this to enforce that contents is an array of strings (each item represents a line in the buffer). Makes it easier to work with plugin APIs. Doing it in a future diff, just a heads up.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


def to_payload(self):
return {
"contents": self.contents,
}

@staticmethod
def from_payload(payload):
return ApplyText(payload["contents"])


def serialize(message):
as_dict = {
"type": message.type.value,
"payload": message.to_payload(),
"version": 1,
}
return json.dumps(as_dict)


def deserialize(data):
try:
as_dict = json.loads(data)
type = as_dict["type"]
payload = as_dict["payload"]

if type == EditorProtocolMessageType.UserChangedEditorText.value:
return UserChangedEditorText.from_payload(payload)

elif type == EditorProtocolMessageType.ApplyTextBuffer.value:
return ApplyTextBuffer.from_payload(payload)

else:
raise EditorProtocolMarshalError

except JSONDecodeError:
raise EditorProtocolMarshalError
50 changes: 50 additions & 0 deletions agent/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import sys
from subprocess import Popen, PIPE
import tandem.protocol.editor.messages as m


def start_agent():
return Popen(
["python3", "main.py"],
stdin=PIPE,
stdout=PIPE,
encoding="utf-8",
)


def send_user_changed(agent_stdin, text):
message = m.UserChangedEditorText(text)
agent_stdin.write(m.serialize(message))
agent_stdin.write("\n")
agent_stdin.flush()


def print_raw_message(agent_stdout):
resp = agent_stdout.readline()
print("Received: " + resp)


def main():
# Spawn the agent process
agent = start_agent()

# Send the agent a dummy message
send_user_changed(agent.stdin, "Hello world!")

# The agent currently just echos messages
# so just print the response
print_raw_message(agent.stdout)

# Repeat
send_user_changed(agent.stdin, "Hello world again!")
print_raw_message(agent.stdout)

# Stop the agent and wait for it to
# shutdown gracefully
agent.stdin.close()
agent.terminate()
agent.wait()


if __name__ == "__main__":
main()
56 changes: 56 additions & 0 deletions plugins/vim/tandem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
import sys

from subprocess import Popen, PIPE

# For now, add the tandem agent path to the system path so that we can use the
# existing messages protocol implementation
tandem_agent_path = os.path.abspath('../../agent')
if tandem_agent_path not in sys.path:
sys.path.insert(0, tandem_agent_path)

import tandem.protocol.editor.messages as m


def start_agent():
return Popen(
["python3", "../../agent/main.py"],
stdin=PIPE,
stdout=PIPE,
)


def send_user_changed(agent_stdin, text):
message = m.UserChangedEditorText(text)
agent_stdin.write(m.serialize(message))
agent_stdin.write("\n")
agent_stdin.flush()


def print_raw_message(agent_stdout):
resp = agent_stdout.readline()
print "Received: " + resp


def main():
# Spawn the agent process
agent = start_agent()

# Send the agent a dummy message
send_user_changed(agent.stdin, "Hello world!")

# The agent currently just echos messages so just print the response
print_raw_message(agent.stdout)

# Repeat
send_user_changed(agent.stdin, "Hello world again!")
print_raw_message(agent.stdout)

# Stop the agent and wait for it to shutdown gracefully
agent.stdin.close()
agent.terminate()
agent.wait()


if __name__ == "__main__":
main()
14 changes: 14 additions & 0 deletions plugins/vim/tandem.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
if !has('python')
" :echom is persistent messaging. See
" http://learnvimscriptthehardway.stevelosh.com/chapters/01.html
:echom "ERROR: Please use a version of Vim with Python support"
finish
endif

if !executable('python3')
:echom "ERROR: Global python3 install required."
finish
endif

" TODO: Use file path relative to .vim file
pyfile tandem.py