├── .gitmodules ├── docs ├── _static │ └── .gitignore ├── .gitignore ├── Makefile ├── index.rst ├── Python.rst ├── Protocol.rst └── conf.py ├── python ├── argo_client │ ├── py.typed │ ├── __init__.py │ ├── netstring.py │ └── interaction.py ├── tests │ ├── __init__.py │ ├── test-data │ │ ├── hello.txt │ │ └── base.txt │ ├── test_file_echo_interaction.py │ └── test_mutable_file_echo_api.py ├── README.md ├── mypy.ini ├── pyproject.toml ├── LICENSE └── CHANGELOG.md ├── tasty-script-exitcode ├── Setup.hs ├── CHANGELOG.md ├── LICENSE ├── tasty-script-exitcode.cabal └── src │ └── Test │ └── Tasty │ └── HUnit │ └── ScriptExit.hs ├── cabal.project ├── java ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── galois │ │ │ └── cryptol │ │ │ └── client │ │ │ ├── connection │ │ │ ├── Unit.java │ │ │ ├── queue │ │ │ │ ├── QueueClosedException.java │ │ │ │ ├── FutureQueue.java │ │ │ │ └── ConcurrentMultiQueue.java │ │ │ ├── UnhandledRpcException.java │ │ │ ├── Pipe.java │ │ │ ├── ConnectionException.java │ │ │ ├── InvalidRpcResponseException.java │ │ │ ├── InvalidRpcCallResultException.java │ │ │ ├── JsonRpcNotification.java │ │ │ ├── JsonRpcException.java │ │ │ ├── netstring │ │ │ │ ├── NetstringPipe.java │ │ │ │ └── Netstring.java │ │ │ ├── json │ │ │ │ └── JsonPipe.java │ │ │ ├── JsonRpcCall.java │ │ │ ├── ConnectionManager.java │ │ │ ├── Connection.java │ │ │ ├── ManagedPipe.java │ │ │ └── JsonConnection.java │ │ │ ├── CryptolException.java │ │ │ ├── Main.java │ │ │ └── CryptolConnection.java │ └── test │ │ └── java │ │ └── com │ │ └── galois │ │ └── cryptol │ │ └── client │ │ ├── LibraryTest.java │ │ └── MultiQueueDemo.java ├── settings.gradle ├── README.md ├── build.gradle ├── gradlew.bat └── gradlew ├── argo ├── CHANGELOG.md ├── src │ └── Argo │ │ ├── Panic.hs │ │ ├── Doc.hs │ │ ├── Doc │ │ ├── ReST.hs │ │ └── Protocol.hs │ │ ├── Netstring.hs │ │ ├── Socket.hs │ │ └── ServerState.hs ├── LICENSE ├── test │ └── Test.hs └── argo.cabal ├── file-echo-api ├── CHANGELOG.md ├── test │ └── Test.hs ├── file-echo-api.cabal ├── mutable-file-echo-api │ └── Main.hs ├── file-echo-api │ └── Main.hs ├── README.rst └── src │ ├── MutableFileEchoServer.hs │ └── FileEchoServer.hs ├── .gitignore ├── stage.sh ├── NEWS.rst ├── README-dist.rst ├── .github └── workflows │ └── ci.yml ├── protocol-notes.org └── README.rst /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | -------------------------------------------------------------------------------- /python/argo_client/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/argo_client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/tests/test-data/hello.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /python/tests/test-data/base.txt: -------------------------------------------------------------------------------- 1 | All your base are belong to us! 2 | -------------------------------------------------------------------------------- /tasty-script-exitcode/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: 2 | argo/ 3 | file-echo-api/ 4 | tasty-script-exitcode/ 5 | -------------------------------------------------------------------------------- /java/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaloisInc/argo/HEAD/java/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /java/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /argo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revision history for argo 2 | 3 | ## 0.1.0.0 -- YYYY-mm-dd 4 | 5 | * First version. Released on an unsuspecting world. 6 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/Unit.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | public class Unit { 4 | public Unit() { } 5 | } 6 | -------------------------------------------------------------------------------- /tasty-script-exitcode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revision history for tasty-script-exitcode 2 | 3 | ## 0.1.0.0 -- YYYY-mm-dd 4 | 5 | * First version. Released on an unsuspecting world. 6 | -------------------------------------------------------------------------------- /java/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /file-echo-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revision history for file-echo-api 2 | 3 | ## 0.1.0.0 -- 2020-10-09 4 | 5 | * First version. Released on an unsuspecting world. A simple echo server which 6 | can load files on disk and send their contents (all or a portion) back 7 | to the client. 8 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Argo Client 2 | 3 | A python library for client connections to [JSON RPC](https://www.jsonrpc.org/specification) servers, specifically designed with the Haskell [Argo](https://github.com/galoisinc/argo) sister library in mind. 4 | 5 | ## Python Version Support 6 | 7 | Currently argo is tested against Python 3.12. 8 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/queue/QueueClosedException.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection.queue; 2 | 3 | public class QueueClosedException extends RuntimeException { 4 | public static final long serialVersionUID = 0; 5 | public QueueClosedException() { 6 | super(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /python/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | no_implicit_optional = True 3 | # Ensure .github/workflows/ci.yml matches the Python version used here. 4 | python_version = 3.12 5 | warn_return_any = True 6 | warn_unused_configs = True 7 | warn_unused_ignores = True 8 | 9 | 10 | # Per-module options: 11 | 12 | [mypy-argo_client.*] 13 | disallow_untyped_defs = True 14 | warn_unreachable = True 15 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/CryptolException.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client; 2 | 3 | public class CryptolException extends Exception { 4 | final String message; 5 | public CryptolException(String message) { 6 | this.message = message; 7 | } 8 | @Override 9 | public String toString() { 10 | return this.message; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/UnhandledRpcException.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import com.galois.cryptol.client.connection.JsonRpcException; 4 | 5 | public class UnhandledRpcException extends RuntimeException { 6 | public static final long serialVersionUID = 0; 7 | public UnhandledRpcException(JsonRpcException e) { super(e); } 8 | } 9 | -------------------------------------------------------------------------------- /java/settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/5.2.1/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = 'cryptol-client' 11 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/Pipe.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import java.util.*; 4 | import java.io.*; 5 | 6 | public interface Pipe extends AutoCloseable { 7 | public void send(A input); 8 | public A receive() throws NoSuchElementException; 9 | @Override 10 | public void close() throws IOException; 11 | public boolean isClosed(); 12 | } 13 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/ConnectionException.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | public class ConnectionException extends RuntimeException { 4 | public static final long serialVersionUID = 0; 5 | public ConnectionException(String e) { super(e); } 6 | public ConnectionException(Throwable e) { super(e); } 7 | public ConnectionException(String m, Throwable e) { super(m, e); } 8 | } 9 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/InvalidRpcResponseException.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | public class InvalidRpcResponseException extends RuntimeException { 4 | public static final long serialVersionUID = 0; 5 | public InvalidRpcResponseException(String e) { super(e); } 6 | public InvalidRpcResponseException(Throwable e) { super(e); } 7 | public InvalidRpcResponseException(String m, Throwable e) { super(m, e); } 8 | } 9 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/InvalidRpcCallResultException.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import com.eclipsesource.json.*; 4 | 5 | public class InvalidRpcCallResultException extends RuntimeException { 6 | public static final long serialVersionUID = 0; 7 | public final JsonValue invalidResult; 8 | public InvalidRpcCallResultException (JsonValue value) { 9 | super(value.toString()); 10 | this.invalidResult = value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /java/src/test/java/com/galois/cryptol/client/LibraryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Java source file was generated by the Gradle 'init' task. 3 | */ 4 | package com.galois.cryptol.client; 5 | 6 | import org.junit.Test; 7 | import static org.junit.Assert.*; 8 | 9 | public class LibraryTest { 10 | @Test public void testSomeLibraryMethod() { 11 | // Library classUnderTest = new Library(); 12 | // assertTrue("someLibraryMethod should return 'true'", classUnderTest.someLibraryMethod()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | dist-*/ 3 | .HTF/ 4 | log/ 5 | .cabal-sandbox/ 6 | .stack-work/ 7 | cabal-dev 8 | *# 9 | *.aux 10 | *.bundle 11 | *.chi 12 | *.chs.h 13 | *.dSYM 14 | *.dylib 15 | *.dyn_hi 16 | *.dyn_o 17 | *.eventlog 18 | *.hi 19 | *.hp 20 | *.o 21 | *.a 22 | *.prof 23 | *.so 24 | *~ 25 | .*.swo 26 | .*.swp 27 | .DS_Store 28 | .hpc 29 | .hsenv 30 | TAGS 31 | cabal.project.local 32 | cabal.sandbox.config 33 | codex.tags 34 | tags 35 | wiki 36 | wip 37 | .ghc.environment.* 38 | .dir-locals.el 39 | __pycache__ 40 | .gradle/ 41 | .meghanada/ 42 | .mypy_cache 43 | python/_build/* 44 | virtenv 45 | .vscode 46 | -------------------------------------------------------------------------------- /stage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | CDIR=dist/cryptol-saw-remote-api 6 | DATE=$(date "+%Y-%m-%d") 7 | CRA=$(cabal v2-exec which cryptol-remote-api) 8 | SRA=$(cabal v2-exec which saw-remote-api) 9 | 10 | mkdir -p ${CDIR} 11 | mkdir -p ${CDIR}/bin 12 | mkdir -p ${CDIR}/doc 13 | mkdir -p ${CDIR}/python 14 | mkdir -p ${CDIR}/examples 15 | 16 | cp ${CRA} ${CDIR}/bin 17 | cp ${SRA} ${CDIR}/bin 18 | cp docs/*.rst ${CDIR}/doc 19 | cp -r python ${CDIR} 20 | cp -r examples ${CDIR} 21 | rm -rf ${CDIR}/python/.stack-work 22 | cp README-dist.rst ${CDIR}/README.rst 23 | 24 | cd dist 25 | tar -czvf cryptol-saw-remote-api-${DATE}.tar.gz cryptol-saw-remote-api 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Argo documentation master file, created by 2 | sphinx-quickstart on Thu Sep 19 16:08:41 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Argo, a JSON-RPC Interface to Cryptol and SAW 7 | ============================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 3 11 | :caption: Contents: 12 | 13 | General-Purpose Python Bindings 14 | Cryptol Python API 15 | SAW Python API 16 | Protocol Overview 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/JsonRpcNotification.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import com.eclipsesource.json.*; 4 | 5 | public class JsonRpcNotification { 6 | // Method name as server knows it (should be constant function) 7 | public String method() { return method; } 8 | // The parameters to this particular invocation (should be constant function) 9 | public JsonValue params() { return params; } 10 | 11 | private final String method; 12 | private final JsonValue params; 13 | 14 | public JsonRpcNotification(String method, JsonValue params) { 15 | this.method = method; 16 | this.params = params; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /java/README.md: -------------------------------------------------------------------------------- 1 | # Java client (currently in stasis) 2 | 3 | This directory is a partial implementation of a java client for the argo 4 | protocol and cryptol/SAW servers thereof. It is currently **in stasis** and is 5 | not being maintained: no guarantees are made about its compliance with the 6 | current implementation, or its development proceeding in lockstep with the work 7 | on the protocol, servers, or other clients. The code is parked here for the aid 8 | of potential future implementors who might wish to revive it. 9 | 10 | # Building the java client 11 | 12 | ``` 13 | $ gradle build 14 | ``` 15 | 16 | # Running the client 17 | 18 | ``` 19 | $ gradle run --console=plain --args=' ' 20 | ``` 21 | -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "argo-client" 3 | version = "0.0.14.99" 4 | readme = "README.md" 5 | keywords = ["JSON", "RPC"] 6 | description = "A JSON RPC client library." 7 | authors = ["Galois, Inc. "] 8 | 9 | license = "BSD License" 10 | include = [ 11 | "LICENSE", 12 | "CHANGELOG.md", 13 | "mypy.ini" 14 | ] 15 | 16 | [tool.poetry.urls] 17 | "Source" = "https://github.com/GaloisInc/argo/tree/master/python" 18 | "Bug Tracker" = "https://github.com/GaloisInc/argo/issues" 19 | 20 | [tool.poetry.dependencies] 21 | python = ">=3.9.0,<4" 22 | 23 | requests = ">=2.31.0" 24 | urllib3 = ">=2.6.0" 25 | types-requests = ">=2.31.0" 26 | 27 | [tool.poetry.dev-dependencies] 28 | mypy = "^1.10" 29 | 30 | [build-system] 31 | requires = ["poetry-core>=1.0.0"] 32 | build-backend = "poetry.core.masonry.api" 33 | -------------------------------------------------------------------------------- /argo/src/Argo/Panic.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | 3 | -- | Provides a means of crashing a program when supposedly-impossible 4 | -- situations arise. 5 | module Argo.Panic (HasCallStack, panic) where 6 | 7 | import Panic hiding (panic) 8 | import qualified Panic as Panic 9 | 10 | data Argo = Argo 11 | 12 | type ArgoPanic = Panic Argo 13 | 14 | -- | Crash the program with a message. 15 | panic :: 16 | HasCallStack => 17 | String {-^ The program site to identify as the source of the panic -} -> 18 | [String] {-^ Further descriptions of why the panic is occurring -} -> 19 | a 20 | panic = Panic.panic Argo 21 | 22 | instance PanicComponent Argo where 23 | panicComponentName _ = "Argo" 24 | panicComponentIssues _ = "https://github.com/GaloisInc/argo/issues" 25 | 26 | {-# Noinline panicComponentRevision #-} 27 | panicComponentRevision = $useGitRevision 28 | -------------------------------------------------------------------------------- /file-echo-api/test/Test.hs: -------------------------------------------------------------------------------- 1 | 2 | module Main ( module Main ) where 3 | 4 | import Test.Tasty 5 | import Test.Tasty.HUnit.ScriptExit 6 | 7 | 8 | import Argo.PythonBindings 9 | import Paths_file_echo_api 10 | 11 | import FileEchoServer() 12 | -- ^ We import FileEchoServer to force rebuild when building 13 | -- the tests in case changes have been made to server. 14 | 15 | main :: IO () 16 | main = 17 | do reqs <- getArgoPythonFile "requirements.txt" 18 | withPython3venv (Just reqs) $ \pip python -> 19 | do pySrc <- getArgoPythonFile "." 20 | testScriptsDir <- getDataFileName "test-scripts/" 21 | pip ["install", pySrc] 22 | putStrLn "pipped" 23 | 24 | scriptTests <- makeScriptTests testScriptsDir [python] 25 | 26 | defaultMain $ 27 | testGroup "Tests for file-echo-api" 28 | [ testGroup "Scripting API tests" scriptTests 29 | ] 30 | -------------------------------------------------------------------------------- /NEWS.rst: -------------------------------------------------------------------------------- 1 | News 2 | ---- 3 | 4 | There has not yet been a release. Please add new features and 5 | externally-visible changes to this list so that we can write release 6 | notes as releases occur, and so that clients of the intermediate 7 | development artifacts can keep apprised of ongoing updates. 8 | 9 | Recent Changes 10 | ============== 11 | 12 | - Added a ``doc`` subcommand that causes documentation to be dumped 13 | about the server. 14 | 15 | - Added a ``--public`` flag that causes the socket server to listen for 16 | external connections 17 | 18 | - Added an HTTP interface. 19 | 20 | - Replaced the default command line interface with a subcommand-based 21 | interface. Now, use the ``stdio``, ``socket``, and ``http`` 22 | subcommands to launch the server in each respective mode. Other 23 | command-line options are unchanged, but validation of nonsensical 24 | combinations is also stricter than it was before. 25 | 26 | - Removed ``--public`` and replaced it with ``--host``. Use ``--host 27 | ::`` or ``--host 0.0.0.0`` to get the previous behavior of 28 | ``--public``. 29 | 30 | - Added a ``--log`` option that controls debug logging, which is now 31 | off by default. 32 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/JsonRpcException.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import com.galois.cryptol.client.connection.*; 4 | import com.galois.cryptol.client.connection.InvalidRpcResponseException; 5 | import com.eclipsesource.json.*; 6 | 7 | public class JsonRpcException extends Exception { 8 | public static final long serialVersionUID = 0; 9 | public final int code; 10 | public final String message; 11 | public final JsonValue data; 12 | 13 | public JsonRpcException(int code, String message, JsonValue data) { 14 | this.code = code; 15 | this.message = message; 16 | this.data = data; 17 | } 18 | 19 | public JsonRpcException(JsonObject error) throws InvalidRpcResponseException { 20 | try { 21 | this.code = error.get("code").asInt(); 22 | this.message = error.get("message").asString(); 23 | this.data = error.get("data"); 24 | } catch (NullPointerException e) { 25 | var msg = "Missing field in error response object: " + error; 26 | throw new InvalidRpcResponseException(msg, e); 27 | } catch (UnsupportedOperationException e) { 28 | var msg = "Bad format for error response object: " + error; 29 | throw new InvalidRpcResponseException(msg, e); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/netstring/NetstringPipe.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection.netstring; 2 | 3 | import java.io.*; 4 | import java.net.*; 5 | import java.util.*; 6 | 7 | import com.galois.cryptol.client.connection.Pipe; 8 | import com.galois.cryptol.client.connection.netstring.*; 9 | 10 | public class NetstringPipe implements Pipe { 11 | 12 | private final InputStream input; 13 | private final OutputStream output; 14 | private boolean closed; 15 | 16 | public NetstringPipe(InputStream input, OutputStream output) { 17 | this.input = input; 18 | this.output = output; 19 | } 20 | 21 | public void send(byte[] bytes) { 22 | try { 23 | Netstring.encodeTo(bytes, this.output); 24 | } catch (IOException e) { 25 | throw new UncheckedIOException(e); 26 | } 27 | } 28 | 29 | public byte[] receive() { 30 | try { 31 | return Netstring.decodeFrom(this.input); 32 | } catch (IOException e) { 33 | throw new UncheckedIOException(e); 34 | } 35 | } 36 | 37 | public void close() throws IOException { 38 | if (!closed) { 39 | input.close(); 40 | output.close(); 41 | closed = true; 42 | } 43 | } 44 | 45 | public boolean isClosed() { 46 | return closed; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /python/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Galois, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/json/JsonPipe.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection.json; 2 | 3 | import java.util.*; 4 | import java.util.function.*; 5 | import java.io.*; 6 | 7 | import com.eclipsesource.json.*; 8 | 9 | import com.galois.cryptol.client.connection.Pipe; 10 | 11 | public class JsonPipe implements Pipe { 12 | 13 | private final Pipe bytePipe; 14 | 15 | public JsonPipe(Pipe bytePipe) { 16 | this.bytePipe = bytePipe; 17 | } 18 | 19 | // The "impossible" exceptions below really should be, because UTF-8 is 20 | // hard-coded, and according to the Java standard must be supported on all 21 | // platforms 22 | 23 | public void send(JsonValue value) { 24 | try { 25 | bytePipe.send(value.toString().getBytes("UTF-8")); 26 | } catch (UnsupportedEncodingException e) { 27 | throw new RuntimeException("Impossible: UTF-8 unsupported"); 28 | } 29 | } 30 | 31 | public JsonValue receive() { 32 | try { 33 | return Json.parse(new String(bytePipe.receive(), "UTF-8")); 34 | } catch (UnsupportedEncodingException e) { 35 | throw new RuntimeException("Impossible: UTF-8 unsupported"); 36 | } 37 | } 38 | 39 | public void close() throws IOException { 40 | this.bytePipe.close(); 41 | } 42 | 43 | public boolean isClosed() { 44 | return this.bytePipe.isClosed(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /java/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This generated file contains a sample Java Library project to get you started. 5 | * For more details take a look at the Java Libraries chapter in the Gradle 6 | * User Manual available at https://docs.gradle.org/5.2.1/userguide/java_library_plugin.html 7 | */ 8 | 9 | plugins { 10 | id 'java-library' 11 | id 'application' 12 | } 13 | 14 | repositories { 15 | // Use jcenter for resolving your dependencies. 16 | // You can declare any Maven/Ivy/file repository here. 17 | mavenCentral() 18 | } 19 | 20 | dependencies { 21 | // This dependency is exported to consumers, that is to say found on their compile classpath. 22 | // api 'org.apache.commons:commons-math3:3.6.1' 23 | 24 | // This dependency is used internally, and not exposed to consumers on their own compile classpath. 25 | implementation 'com.eclipsesource.minimal-json:minimal-json:0.9.5' 26 | 27 | // Use JUnit test framework 28 | testImplementation 'junit:junit:4.12' 29 | } 30 | 31 | application { 32 | mainClassName = 'com.galois.cryptol.client.Main' 33 | } 34 | 35 | run { 36 | standardInput = System.in 37 | } 38 | 39 | task(queuedemo, dependsOn: 'classes', type: JavaExec) { 40 | main = 'com.galois.cryptol.client.MultiQueueDemo' 41 | classpath = sourceSets.test.runtimeClasspath 42 | standardInput = System.in 43 | } 44 | 45 | tasks.withType(JavaCompile) { 46 | options.compilerArgs << '-Xdiags:verbose' 47 | options.compilerArgs << '-Xlint:unchecked' 48 | } 49 | -------------------------------------------------------------------------------- /argo/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Galois, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /tasty-script-exitcode/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Galois, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/JsonRpcCall.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import java.util.function.*; 4 | 5 | import com.eclipsesource.json.*; 6 | import com.galois.cryptol.client.connection.*; 7 | 8 | public class JsonRpcCall extends JsonRpcNotification { 9 | // How to convert the server response to the function call result; should 10 | // return null if it cannot be decoded 11 | public O decode(JsonValue o) { return this.decoder.apply(o); }; 12 | // How to convert any server-returned exception to either a result, or a 13 | // custom exception -- returning null will cause the original 14 | // JsonRpcException to be rethrown as a runtime exception (this should only 15 | // be done if the server violates protocol and sends a truly unexpected 16 | // error not encompassed by E) 17 | public E handle(JsonRpcException e) { return this.handler.apply(e); } 18 | 19 | protected final Function decoder; 20 | protected final Function handler; 21 | 22 | public JsonRpcCall(String method, JsonValue params, 23 | Function decode, 24 | Function handle) { 25 | super(method, params); 26 | this.decoder = decode; 27 | this.handler = handle; 28 | } 29 | 30 | public JsonRpcCall(JsonRpcNotification notification, 31 | Function decode, 32 | Function handle) { 33 | super(notification.method(), notification.params()); 34 | this.decoder = decode; 35 | this.handler = handle; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/Python.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | General-Purpose Python Bindings 3 | =============================== 4 | 5 | Module ``argo.connection`` 6 | ========================== 7 | 8 | This module contains utilities that are useful for connecting to servers that implement the Argo protocols. To interact with a server, a process and a connection are necessary. The variants of processes are used to manage the different transport layers that are available, such as sockets or pipes, as well as server process lifecycles. They are: 9 | 10 | :class:`argo.connection.DynamicSocketProcess` 11 | This is used to start a subprocess and communicate with it over a socket on a port chosen by the server itself. 12 | 13 | :class:`argo.connection.RemoteSocketProcess` 14 | This is used to connect to a socket on a pre-existing server process, potentially on another machine 15 | 16 | :class:`argo.connection.StdIOProcess` 17 | This is used to manage a server subprocess, communicating with it over Unix pipes. 18 | 19 | The other necessary component is a :class:`argo.connection.ServerConnection`. While a process manages the underlying transport layer, the connection object tracks the logical state of the connection itself, encoding protocol details that are invariant with respect to the transport method. This includes mapping request IDs to their responses and JSON (de)serialization. 20 | 21 | .. automodule:: argo.connection 22 | :members: 23 | :special-members: 24 | 25 | Module ``argo.interaction`` 26 | =========================== 27 | 28 | .. automodule:: argo.interaction 29 | :members: 30 | :special-members: 31 | 32 | Module ``argo.netstring`` 33 | ========================= 34 | 35 | .. automodule:: argo.netstring 36 | :members: 37 | :special-members: 38 | -------------------------------------------------------------------------------- /tasty-script-exitcode/tasty-script-exitcode.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | -- Initial package description 'tasty-script-exitcode.cabal' generated by 3 | -- 'cabal init'. For further documentation, see 4 | -- http://haskell.org/cabal/users-guide/ 5 | 6 | name: tasty-script-exitcode 7 | version: 0.1.0.0 8 | synopsis: Run a bunch of unit tests written in the scripting language 9 | of your choice 10 | description: This package defines some convenient functions for writing 11 | tasty-hunit tests that invoke an external scripting 12 | language interpreter and succeed or fail based upon the 13 | exit code of that script. A test suite made of scripts can 14 | be created by making a directory full of test files to be 15 | run with a particular interpreter. This can be useful for 16 | writing integration tests that play with your program 17 | "from the outside" or for testing your program's API to 18 | other languages. 19 | -- bug-reports: 20 | license: BSD-3-Clause 21 | license-file: LICENSE 22 | author: Kenny Foner 23 | maintainer: kwf@galois.com 24 | -- copyright: 25 | category: Testing 26 | extra-source-files: CHANGELOG.md 27 | 28 | library 29 | exposed-modules: Test.Tasty.HUnit.ScriptExit 30 | -- other-modules: 31 | other-extensions: NamedFieldPuns 32 | build-depends: base >=4.12, 33 | process >=1.6.3.0, 34 | filepath >=1.4.2.1, 35 | directory >=1.3.3.0, 36 | containers >=0.6.0.1, 37 | tasty-hunit >= 0.10, 38 | tasty >= 1.2, 39 | temporary >= 1.3 40 | hs-source-dirs: src 41 | default-language: Haskell2010 42 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/Main.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client; 2 | 3 | import java.util.*; 4 | import java.io.*; 5 | import java.net.*; 6 | import java.util.concurrent.*; 7 | import java.util.function.*; 8 | 9 | import com.eclipsesource.json.*; 10 | import com.galois.cryptol.client.*; 11 | import com.galois.cryptol.client.connection.queue.*; 12 | 13 | class Main { 14 | public static void main(String[] args) { 15 | if (args.length == 2) { 16 | String cryptolServer = args[0]; 17 | String startingDir = args[1]; 18 | cryptolEval(cryptolServer, startingDir); 19 | } else { 20 | var error = "Please specify a server executable and starting directory"; 21 | System.err.println(error); 22 | System.exit(1); 23 | } 24 | } 25 | 26 | public static void cryptolEval(String server, String dir) { 27 | try(CryptolConnection c = new CryptolConnection(server, new File(dir))) { 28 | Scanner in = new Scanner(System.in); 29 | 30 | boolean loaded = false; 31 | do { 32 | System.out.print("Load module: "); 33 | try { 34 | c.loadModule(in.nextLine()); 35 | loaded = true; 36 | } catch (CryptolException e) { 37 | System.out.println(e); 38 | } 39 | } while (!loaded); 40 | 41 | System.out.print("Evaluate: "); 42 | while (in.hasNextLine()) { 43 | var line = in.nextLine(); 44 | try { 45 | System.out.println(c.evalExpr(line)); 46 | } catch (CryptolException e) { 47 | System.out.println(e); 48 | } 49 | System.out.print("Evaluate: "); 50 | } 51 | } catch (Exception e) { 52 | throw new RuntimeException(e); 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /file-echo-api/file-echo-api.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | name: file-echo-api 3 | version: 0.1.0.0 4 | license: BSD-3-Clause 5 | license-file: LICENSE 6 | author: Andrew Kent 7 | maintainer: andrew@galois.com 8 | category: Language 9 | extra-source-files: CHANGELOG.md 10 | data-files: test-scripts/**/*.py 11 | test-scripts/**/*.txt 12 | 13 | common warnings 14 | ghc-options: 15 | -Wall 16 | -Wno-missing-exported-signatures 17 | -Wno-missing-import-lists 18 | -Wno-missed-specialisations 19 | -Wno-all-missed-specialisations 20 | -Wno-unsafe 21 | -Wno-safe 22 | -Wno-missing-local-signatures 23 | -Wno-monomorphism-restriction 24 | -Wno-implicit-prelude 25 | 26 | common deps 27 | build-depends: 28 | base >=4.11.1.0 && <4.20, 29 | argo, 30 | aeson >= 1.4.2, 31 | bytestring >= 0.10.8 && < 0.13, 32 | containers >=0.5.11 && <0.7, 33 | directory ^>= 1.3.1, 34 | optparse-applicative >= 0.14 && < 0.19, 35 | scientific ^>= 0.3, 36 | text >= 1.2.3 && < 2.2, 37 | time, 38 | unordered-containers ^>= 0.2, 39 | vector ^>= 0.13, 40 | 41 | default-language: Haskell2010 42 | 43 | library 44 | import: deps, warnings 45 | hs-source-dirs: src 46 | 47 | exposed-modules: 48 | FileEchoServer, 49 | MutableFileEchoServer 50 | 51 | executable file-echo-api 52 | import: deps, warnings 53 | main-is: Main.hs 54 | hs-source-dirs: file-echo-api 55 | build-depends: 56 | file-echo-api 57 | other-modules: 58 | Paths_file_echo_api 59 | 60 | executable mutable-file-echo-api 61 | import: deps, warnings 62 | main-is: Main.hs 63 | hs-source-dirs: mutable-file-echo-api 64 | build-depends: 65 | file-echo-api 66 | other-modules: 67 | Paths_file_echo_api 68 | -------------------------------------------------------------------------------- /README-dist.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | This package contains servers that wrap the functionality of Cryptol and 5 | SAW within a JSON-RPC API, along with a Python client library for 6 | convenient interaction with that API. 7 | 8 | Environment Setup 9 | ================= 10 | 11 | The package contains two directories that external tools need to be able 12 | to find: `bin` and `python`. The former contains the server executables, 13 | and the latter contains Python code to make it convenient to interact 14 | with the servers. 15 | 16 | To make these accessible, the easiest approach is to set the `PATH` 17 | environment variable to include the `bin` directory, wherever you unpack 18 | it, and set the `PYTHONPATH` environment variable to point to 19 | 20 | Dependencies 21 | ============ 22 | 23 | The server executables are standalone and dynamically link to only a few 24 | core system libraries. The Python code requires Python 3.7 or newer, and 25 | the following extra packages: 26 | 27 | - BitVector==3.4.9 28 | - mypy==0.730 29 | 30 | You can install these either globally or within a Python `virtualenv` 31 | containter using 32 | 33 | `pip install -r python/requirements.txt`. 34 | 35 | If you set `PYTHONPATH` as described above, the Python interpreter will 36 | be able to find the client library code. Alternatively, you can install 37 | the code for the Python client libraries, also either globally or within 38 | a `virtualenv`, using 39 | 40 | `pip install -e python/` 41 | 42 | Running Examples 43 | ================ 44 | 45 | Once you have installed the necessary dependencies and set up your 46 | environment variables as described above, the scripts in the `examples` 47 | directory should be directly runnable. The `.py` files in that directory 48 | are executable and have an interpreter directive to tell the shell to 49 | run them with `python3`. 50 | 51 | When the verification scripts run, they will print a URL which will show 52 | you a graphical indication of verification status as it progresses. For 53 | this to work, the Graphviz `dot` program must be in your `PATH`. 54 | -------------------------------------------------------------------------------- /python/argo_client/netstring.py: -------------------------------------------------------------------------------- 1 | """Argo uses D. J. Berstein's `netstrings `_ 2 | as a lightweight transport layer for JSON RPC. 3 | """ 4 | 5 | from typing import Optional, Tuple 6 | 7 | def encode(string : str) -> bytes: 8 | """Encode a ``str`` into a netstring. 9 | 10 | >>> encode("hello") 11 | b'5:hello,' 12 | """ 13 | bytestring = string.encode() 14 | return str(len(bytestring)).encode() + b':' + bytestring + b',' 15 | 16 | class InvalidNetstring(Exception): 17 | """Exception for malformed netstrings""" 18 | def __init__(self, message: str): 19 | self.message = message 20 | super().__init__(self.message) 21 | 22 | def decode(netstring : bytes) -> Optional[Tuple[str, bytes]]: 23 | """Decode the first valid netstring from a bytestring, returning its 24 | string contents and the remainder of the bytestring. 25 | 26 | Returns None when the bytes are a prefix of a valid netstring. 27 | 28 | Raises InvalidNetstring when the bytes are not a prefix of a valid 29 | netstring. 30 | 31 | >>> decode(b'5:hello,more') 32 | ('hello', b'more') 33 | 34 | """ 35 | 36 | colon = netstring.find(b':') 37 | if colon == -1 and len(netstring) >= 10 or colon >= 10: 38 | # Avoid cases where the incomplete length is already too 39 | # long or the length is complete but is too long. 40 | # A minimum ten-digit length will be approximately 1GB or more 41 | # which is larger than we should need to handle for this API 42 | raise InvalidNetstring("message length too long") 43 | 44 | if colon == -1: 45 | # incomplete length, wait for more bytes 46 | return None 47 | 48 | lengthstring = netstring[0:colon] 49 | if colon == 0 or not lengthstring.isdigit(): 50 | raise InvalidNetstring("invalid format, malformed message length") 51 | 52 | length = int(lengthstring) 53 | comma = colon + length + 1 54 | if len(netstring) <= comma: 55 | # incomplete message, wait for more bytes 56 | return None 57 | 58 | if netstring[comma] != 44: # comma 59 | raise InvalidNetstring("invalid format, missing comma") 60 | 61 | return (netstring[colon + 1 : comma].decode(), netstring[comma+1:]) 62 | -------------------------------------------------------------------------------- /python/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # next (TBA) 2 | + Bump `urllib3` to 2.6.0 for a security advisory, and drop Python 3.8, 3 | which is long EOL; we now require at least Python 3.9. 4 | 5 | # argo-client v0.0.14 (6 Nov 2025) 6 | + Allow user-specified global options in Argo's CLI 7 | 8 | # argo-client v0.0.13 (17 Sep 2024) 9 | + Use blocking IO to reduce CPU load when receiving replies 10 | + wait_for_reply_to now consumes the reply waited for. previously the reply was held in memory indefinitely 11 | + Removes RemoteSocketProcess.buffer_replies method. Replies are processed during wait_for_reply_to 12 | 13 | # argo-client v0.0.12 (15 May 2024) 14 | + Bump `mypy` to `mypy-1.10`, and update its dependencies to support the bump. This allows for Python 3.12 support. 15 | 16 | 17 | # argo-client v0.0.11 (30 Jan 2023) 18 | + Bump the lower version bounds on various dependencies. `argo-client` now 19 | requires Python 3.8 and `mypy-0.991` as the minimum. This has the benefit 20 | of eliminating the `typed-ast` library as a dependency before `typed-ast` 21 | is EOL'd. 22 | 23 | # argo-client v0.0.10 (9 Dec 2021) 24 | + Use poetry to manage package, bump dependency bounds. 25 | 26 | # argo-client v0.0.9 (13 Sep 2021) 27 | + Add an optional `timeout` keyword argument to `Interaction`'s 28 | `__init__` method which passes any specified timeout to the 29 | contained `send_command` call. 30 | 31 | # argo-client v0.0.8 (10 Sep 2021) 32 | + Add a `timeout` keyword to `argo_client.connection`'s 33 | `send_command` and `send_query` to support user-specified 34 | maximum durations for individual requests. 35 | 36 | # argo-client v0.0.7 (25 Aug 2021) 37 | + Change the behavior of the `Command` `state` method so that after a `Command` 38 | raises an exception, subsequent interactions will not also raise the same 39 | exception. 40 | 41 | # argo-client v0.0.6 (22 Jul 2021) 42 | + Add logging option to client. See `ServerProcess.logging(..)` and 43 | `ServerConnection.logging(..)`. 44 | 45 | # argo-client v0.0.5 (23 Jun 2021) 46 | + Add HTTP flag for TLS cert verification options. 47 | 48 | # argo-client v0.0.4 (2 Mar 2021) 49 | + Add support for notifications. 50 | + Improved error handling. 51 | 52 | # argo-client v0.0.3 (9 Feb 2021) 53 | + Add mypy annotations to package for argo_client module. 54 | 55 | # argo-client v0.0.2 (9 Feb 2021) 56 | + Initial release. 57 | -------------------------------------------------------------------------------- /argo/test/Test.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | module Main where 4 | 5 | import Data.Aeson as JSON (fromJSON, toJSON, Result(..), Value(..)) 6 | 7 | import Data.ByteString.Lazy (ByteString) 8 | import qualified Data.HashMap.Strict as HM 9 | import Data.List.NonEmpty(NonEmpty(..)) 10 | 11 | 12 | import Test.QuickCheck.Instances.ByteString 13 | import Test.QuickCheck.Instances.Scientific 14 | import Test.QuickCheck.Instances.Text 15 | import Test.Tasty 16 | import Test.Tasty.QuickCheck 17 | import Test.Tasty.HUnit 18 | 19 | import Argo 20 | import Argo.Netstring 21 | import Argo.ServerState 22 | 23 | 24 | main :: IO () 25 | main = defaultMain tests 26 | 27 | tests :: TestTree 28 | tests = testGroup "Tests for Argo" [ netstringProps, jsonRPCProps, stateIDProps ] 29 | 30 | netstringProps :: TestTree 31 | netstringProps = 32 | testGroup "QuickCheck properties for netstrings" 33 | [ testProperty "fromNetstring . toNetstring is identity, with no leftover " $ 34 | \ (bs :: ByteString) -> parseNetstring (encodeNetstring (netstring bs)) == (netstring bs, "") 35 | , testProperty "doubly encoding and decoding is identity, with no leftover at either step " $ 36 | \ (bs :: ByteString) -> 37 | let (once, rest) = parseNetstring (encodeNetstring (netstring bs)) 38 | in rest == "" && parseNetstring (encodeNetstring once) == (netstring bs, "") 39 | , testProperty "decoding leaves the right amount behind" $ 40 | \ (bs :: ByteString) (rest :: ByteString) -> 41 | parseNetstring ((encodeNetstring (netstring bs)) <> rest) == (netstring bs, rest) 42 | ] 43 | 44 | instance Arbitrary RequestID where 45 | arbitrary = 46 | oneof [ IDText <$> arbitrary 47 | , IDNum <$> arbitrary 48 | , pure IDNull 49 | ] 50 | 51 | 52 | jsonRPCProps :: TestTree 53 | jsonRPCProps = 54 | testGroup "QuickCheck properties for JSONRPC" 55 | [ testProperty "encoding and decoding request IDs is the identity" $ 56 | \(rid :: RequestID) -> 57 | case fromJSON (toJSON rid) of 58 | JSON.Success v -> rid == v 59 | JSON.Error err -> False 60 | ] 61 | 62 | stateIDProps :: TestTree 63 | stateIDProps = 64 | testGroup "Serialization and deserialization for state IDs" 65 | [ testCase "Initial state (de)serialization" $ 66 | JSON.Success initialStateID @?= fromJSON JSON.Null 67 | ] 68 | -------------------------------------------------------------------------------- /java/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/ConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import java.lang.Runtime.*; 4 | import java.util.*; 5 | import java.io.*; 6 | import java.util.function.*; 7 | 8 | public class ConnectionManager implements AutoCloseable { 9 | 10 | private final PipeFactory newPipe; 11 | private final ProcessBuilder builder; 12 | 13 | private volatile Process process; 14 | private volatile Pipe currentPipe; 15 | private volatile boolean closed = false; 16 | 17 | @FunctionalInterface 18 | public static interface PipeFactory { 19 | public Pipe make(OutputStream in, InputStream out, InputStream err) 20 | throws IOException; 21 | } 22 | 23 | private void destroyProcess() { 24 | if (process != null && !closed) { 25 | // System.err.println("Destroying process: " + process.pid()); 26 | process.destroy(); 27 | if (process.isAlive()) { 28 | process.destroyForcibly(); 29 | } 30 | process = null; 31 | } 32 | } 33 | 34 | public synchronized Pipe get() throws IOException { 35 | if (!closed) { 36 | // Destroy the old process 37 | destroyProcess(); 38 | // Create the new process 39 | process = builder.start(); 40 | // System.err.println("Created process: " + process.pid()); 41 | var in = process.getOutputStream(); 42 | var out = process.getInputStream(); 43 | var err = process.getErrorStream(); 44 | return currentPipe = newPipe.make(in, out, err); 45 | } else { 46 | throw new IllegalStateException("Connection manager is closed"); 47 | } 48 | } 49 | 50 | public ConnectionManager(ProcessBuilder builder, PipeFactory newPipe) 51 | throws IOException { 52 | this.newPipe = newPipe; 53 | this.builder = builder; 54 | // Ensure that the current process gets killed with the JVM process 55 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 56 | try { 57 | this.close(); 58 | } catch (IOException e) { 59 | throw new UncheckedIOException(e); 60 | } 61 | })); 62 | } 63 | 64 | @Override 65 | public synchronized void close() throws IOException { 66 | if (!closed) { 67 | if (currentPipe != null) { 68 | currentPipe.close(); 69 | } 70 | currentPipe = null; 71 | destroyProcess(); 72 | closed = true; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /argo/argo.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.4 2 | name: argo 3 | version: 0.1.0.0 4 | license: BSD-3-Clause 5 | license-file: LICENSE 6 | author: David Thrane Christiansen 7 | maintainer: dtc@galois.com 8 | category: Network 9 | extra-source-files: CHANGELOG.md 10 | 11 | common errors 12 | ghc-options: 13 | -Werror=missing-fields 14 | -Werror=incomplete-patterns 15 | -Werror=missing-methods 16 | -Werror=overlapping-patterns 17 | 18 | common warnings 19 | ghc-options: 20 | -Wall 21 | -Wno-missing-exported-signatures 22 | -Wno-missing-import-lists 23 | -Wno-missed-specialisations 24 | -Wno-all-missed-specialisations 25 | -Wno-unsafe 26 | -Wno-safe 27 | -Wno-missing-local-signatures 28 | -Wno-monomorphism-restriction 29 | -Wno-implicit-prelude 30 | -Wno-missing-deriving-strategies 31 | 32 | common deps 33 | build-depends: 34 | base >= 4.14 && < 4.20, 35 | aeson >= 1.4.2 && < 2.3, 36 | async ^>= 2.2, 37 | bytestring >= 0.10.8 && < 0.13, 38 | containers >= 0.5.11 && <0.7, 39 | directory ^>= 1.3, 40 | filelock ^>= 0.1, 41 | filepath ^>= 1.4, 42 | hashable >= 1.2 && < 1.5, 43 | http-types ^>= 0.12, 44 | mtl >= 2.2 && < 2.4, 45 | network >= 3.0.1 && < 3.3, 46 | optparse-applicative >= 0.14 && < 0.19, 47 | panic, 48 | safe ^>= 0.3, 49 | scientific ^>= 0.3, 50 | scotty >= 0.12 && < 0.23, 51 | silently ^>= 1.2, 52 | text >= 1.2.3 && < 2.2, 53 | unordered-containers ^>= 0.2, 54 | uuid ^>= 1.3, 55 | warp >= 3.0.14, 56 | warp-tls >= 3.1.0 57 | 58 | 59 | library 60 | import: deps, warnings, errors 61 | exposed-modules: 62 | Argo 63 | Argo.DefaultMain 64 | Argo.Doc 65 | Argo.Doc.Protocol 66 | Argo.Doc.ReST 67 | Argo.Panic 68 | Argo.ServerState 69 | Argo.Socket 70 | Argo.Netstring 71 | hs-source-dirs: src 72 | default-language: Haskell2010 73 | 74 | 75 | test-suite test-argo 76 | import: deps, warnings, errors 77 | type: exitcode-stdio-1.0 78 | default-language: Haskell2010 79 | hs-source-dirs: test 80 | main-is: Test.hs 81 | build-depends: argo, 82 | quickcheck-instances, 83 | tasty, 84 | tasty-hunit, 85 | tasty-quickcheck, 86 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: argo 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build-linux: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ghc-ver: ["8.10.7", "9.0.2", "9.2.8", "9.4.5", "9.6.2", "9.8.1"] 15 | cabal-ver: ["3.10.1.0"] 16 | # complete all jobs 17 | fail-fast: false 18 | name: Argo - GHC v${{ matrix.ghc-ver }} - ubuntu-latest 19 | steps: 20 | - uses: actions/setup-python@v2 21 | with: 22 | # Ensure pyproject.toml and python/mypy.ini are kept in sync with the Python version here. 23 | python-version: '3.12' 24 | - uses: abatilo/actions-poetry@v2.1.4 25 | with: 26 | poetry-version: 1.4.2 27 | - name: Checkout 28 | uses: actions/checkout@v2 29 | with: 30 | submodules: true 31 | - name: Get GHC and Cabal 32 | uses: haskell/actions/setup@v1 33 | id: setup-haskell 34 | with: 35 | ghc-version: ${{ matrix.ghc-ver }} 36 | cabal-version: ${{ matrix.cabal-ver }} 37 | - uses: actions/cache/restore@v3 38 | name: Restore cabal store cache 39 | with: 40 | path: /home/runner/.cabal/store/ghc-${{ matrix.ghc-ver }} 41 | # Prefer previous SHA hash if it is still cached 42 | key: linux-${{ matrix.ghc-ver }}-${{ hashFiles('cabal.project.freeze') }}-${{ github.sha }} 43 | # otherwise just use most recent build. 44 | restore-keys: linux-${{ matrix.ghc-ver }}-${{ hashFiles('cabal.project.freeze') }} 45 | - name: Cabal update 46 | run: cabal update 47 | # Build macaw-base dependencies and crucible separately just so later 48 | # steps are less verbose and major dependency failures are separate. 49 | - name: Install python dependencies 50 | working-directory: ./python 51 | # see https://github.com/python-poetry/poetry/issues/4210 for new-installer workaround details below 52 | run: | 53 | poetry config experimental.new-installer false 54 | poetry install 55 | - name: Typecheck python code 56 | working-directory: ./python 57 | run: poetry run mypy --install-types --non-interactive argo_client 58 | - name: Configure 59 | run: | 60 | cabal configure --enable-tests 61 | - name: Build 62 | run: | 63 | cabal build all 64 | - name: Cabal argo tests 65 | run: cabal test argo 66 | - name: Python argo-client unit tests 67 | working-directory: ./python 68 | run: poetry run python -m unittest discover --verbose tests 69 | - uses: actions/cache/save@v3 70 | name: Save cabal store cache 71 | if: always() 72 | with: 73 | path: /home/runner/.cabal/store/ghc-${{ matrix.ghc-ver }} 74 | # Prefer previous SHA hash if it is still cached 75 | key: linux-${{ matrix.ghc-ver }}-${{ hashFiles('cabal.project.freeze') }}-${{ github.sha }} 76 | -------------------------------------------------------------------------------- /file-echo-api/mutable-file-echo-api/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE PartialTypeSignatures #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | module Main ( main ) where 6 | 7 | import Data.ByteString (ByteString) 8 | import Data.Version (showVersion) 9 | import qualified Options.Applicative as Opt 10 | 11 | import qualified Argo as Argo 12 | import qualified Argo.Doc as Doc 13 | import Argo.DefaultMain ( customMain, userOptions ) 14 | 15 | import qualified MutableFileEchoServer as MFES 16 | import qualified Paths_file_echo_api 17 | 18 | main :: IO () 19 | main = customMain parseServerOptions parseServerOptions parseServerOptions parseServerOptions version description getApp 20 | where 21 | getApp opts = 22 | Argo.mkApp 23 | "mutable-file-echo-api" 24 | docs 25 | (Argo.defaultAppOpts Argo.MutableState) 26 | (mkInitState $ userOptions opts) 27 | serverMethods 28 | 29 | docs :: [Doc.Block] 30 | docs = 31 | [ Doc.Paragraph [Doc.Text "A sample server that demonstrates filesystem caching with a mutable application state."] 32 | ] 33 | 34 | description :: String 35 | description = 36 | "An RPC server for loading and printing files." 37 | 38 | mkInitState :: ServerOptions -> (FilePath -> IO ByteString) -> IO MFES.ServerState 39 | mkInitState opts reader = MFES.initialState (initialFile opts) reader 40 | 41 | newtype ServerOptions = ServerOptions { initialFile :: Maybe FilePath } 42 | 43 | -- This function parses additional options used by this particular 44 | -- application. The ordinary Argo options are still parsed, and these 45 | -- are appended. 46 | parseServerOptions :: Opt.Parser ServerOptions 47 | parseServerOptions = ServerOptions <$> filename 48 | where 49 | filename = 50 | Opt.optional $ Opt.strOption $ 51 | Opt.long "file" <> 52 | Opt.metavar "FILENAME" <> 53 | Opt.help "Initial file to echo" 54 | 55 | -- Display the version number when the --version option is supplied. 56 | version :: Opt.Parser (a -> a) 57 | version = Opt.simpleVersioner (showVersion Paths_file_echo_api.version) 58 | 59 | serverMethods :: [Argo.AppMethod MFES.ServerState] 60 | serverMethods = 61 | [ Argo.command "load" (Doc.Paragraph [Doc.Text "Load a file from disk into memory."]) MFES.loadCmd 62 | , Argo.command "clear" (Doc.Paragraph [Doc.Text "Forget the loaded file."]) MFES.clearCmd 63 | , Argo.command "slow clear" (Doc.Paragraph [Doc.Text "Forgets the loaded file slowly (i.e., char by char)."]) MFES.slowClear 64 | , Argo.query "show" (Doc.Paragraph [Doc.Text "Show a substring of the file."]) MFES.showCmd 65 | , Argo.query "sleep query" (Doc.Paragraph [Doc.Text "Sleep for a specified number of microseconds."]) MFES.sleepQuery 66 | , Argo.notification "destroy state" (Doc.Paragraph [Doc.Text "Destroy a state."]) MFES.destroyState 67 | , Argo.notification "interrupt" (Doc.Paragraph [Doc.Text "Interrupt all threads in the server."]) MFES.interruptAllThreads 68 | ] 69 | -------------------------------------------------------------------------------- /argo/src/Argo/Doc.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE AllowAmbiguousTypes #-} 2 | {-# LANGUAGE DefaultSignatures #-} 3 | {-# LANGUAGE FunctionalDependencies #-} 4 | {-# LANGUAGE GADTs #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE TypeApplications #-} 7 | {-# LANGUAGE TypeOperators #-} 8 | 9 | module Argo.Doc (LinkTarget(..), Block(..), Inline(..), Described(..), DescribedMethod(..), datatype) where 10 | 11 | import Data.List.NonEmpty 12 | import Data.Text (Text) 13 | import Data.Typeable 14 | 15 | data LinkTarget 16 | = URL Text 17 | | TypeDesc TypeRep 18 | 19 | data Block 20 | = Section Text [Block] 21 | | Datatype TypeRep Text [Block] 22 | | App Text [Block] 23 | | Paragraph [Inline] 24 | | DescriptionList [(NonEmpty Inline, Block)] 25 | | BulletedList [Block] 26 | 27 | data Inline 28 | = Text Text 29 | | Link LinkTarget Text 30 | | Literal Text 31 | 32 | -- | This class provides the canonical documentation for a datatype 33 | -- that occurs as part of a protocol message (in other words, it 34 | -- documents the @FromJSON@ and @ToJSON@ instances). The type variable 35 | -- does not occur in the method's signature because it is intended to 36 | -- be used with the @TypeApplications@ extension to GHC Haskell. 37 | class Described a where 38 | typeName :: Text 39 | description :: [Block] 40 | 41 | 42 | -- | This class provides the canonical documentation for a pair of datatypes, 43 | -- where: 44 | -- 45 | -- * The first datatype (@params@) is deserialized as parameters to some method 46 | -- via a @FromJSON@ instance, and 47 | -- 48 | -- * The second datatype (@result@) is serialized as the result returned by the 49 | -- same method via a @ToJSON@ instance. 50 | -- 51 | -- The @params@ type is almost always a custom data type defined for the 52 | -- purpose of interfacing with RPC, which is why @result@ has a functional 53 | -- dependency on @params@. On the other hand, it is common for @result@ to be 54 | -- off-the-shelf data types such as @Value@ (for methods that return a JSON 55 | -- object) or @()@ (for methods that do not return any values). 56 | -- 57 | -- Neither @params@ nor @result@ occur in the signatures of the methods 58 | -- because they are intended to be used with the @TypeApplications@ extension 59 | -- to GHC Haskell. 60 | class DescribedMethod params result | params -> result where 61 | -- | Documentation for the parameters expected by the method. 62 | parameterFieldDescription :: [(Text, Block)] 63 | 64 | -- | Documentation for the result returned by the method. 65 | -- 66 | -- If the method does not return anything—that is, if @result@ is 67 | -- @()@—then this method does not need to be implemented, as it will be 68 | -- defaulted appropriately. 69 | resultFieldDescription :: [(Text, Block)] 70 | default resultFieldDescription :: (result ~ ()) => [(Text, Block)] 71 | resultFieldDescription = [] 72 | 73 | datatype :: forall a . (Typeable a, Described a) => Block 74 | datatype = 75 | Datatype (typeRep (Proxy @a)) (typeName @a) (description @a) 76 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/queue/FutureQueue.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection.queue; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.*; 5 | 6 | import com.galois.cryptol.client.connection.*; 7 | 8 | // Queue returning futures for popped items: if the queue is empty when you pop 9 | // from it, the returned future will be fulfilled by a future insertion 10 | // operation 11 | 12 | public class FutureQueue { 13 | 14 | // Invariant: if credit is not empty, debt is empty, and vice-versa 15 | private final Queue credit; 16 | private final Queue> debt; 17 | private boolean closed = false; 18 | 19 | public FutureQueue() { 20 | this.credit = new ArrayDeque(); 21 | this.debt = new ArrayDeque>(); 22 | } 23 | 24 | public FutureQueue(Collection c) { 25 | this.credit = new ArrayDeque(c); 26 | this.debt = new ArrayDeque>(); 27 | } 28 | 29 | public boolean isEmpty() { 30 | return this.balance() == 0; 31 | } 32 | 33 | public boolean isClosed() { 34 | return this.closed; 35 | } 36 | 37 | public int balance() { 38 | return credit.size() - debt.size(); 39 | } 40 | 41 | // Put a value into the queue, either fulfilling a waiting promise, or 42 | // adding to the list of items yet to be dequeued 43 | public void put(E e) throws QueueClosedException { 44 | if (!closed) { 45 | if (debt.isEmpty()) { 46 | credit.add(e); 47 | } else { 48 | debt.remove().complete(e); 49 | } 50 | } else { 51 | throw new QueueClosedException(); 52 | } 53 | } 54 | 55 | // Return a future corresponding to the next element of the queue, whether 56 | // or not that element has already been added to the queue. Once the 57 | // corresponding put() has been executed, the future will be fulfilled with 58 | // that value. 59 | public Future takeFuture() { 60 | var e = new CompletableFuture(); 61 | if (credit.isEmpty()) { 62 | if (!closed) { 63 | debt.add(e); 64 | } else { 65 | e.cancel(false); 66 | } 67 | } else { 68 | e.complete(credit.remove()); 69 | } 70 | return e; 71 | } 72 | 73 | // Closes the queue, so that future put() operations don't do anything, and 74 | // cancels all debt futures, including any produced by future calls to 75 | // takeFuture() 76 | // This is totally fine to run multiple times 77 | public boolean close() { 78 | // Cancel all existing debt, causing all waiting promises to throw 79 | // exceptions immediately 80 | for (var f : debt) f.cancel(false); 81 | this.debt.clear(); 82 | boolean wasClosed = this.isClosed(); 83 | this.closed = true; 84 | return wasClosed; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/netstring/Netstring.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection.netstring; 2 | 3 | import java.util.ArrayList; 4 | import java.util.ArrayDeque; 5 | import java.util.Iterator; 6 | import java.util.NoSuchElementException; 7 | import java.io.*; 8 | 9 | public class Netstring { 10 | 11 | // Encode a byte array in netstring format and output to a stream 12 | public static void encodeTo(byte[] bytes, OutputStream output) 13 | throws IOException { 14 | // Calculate the array of length bytes 15 | var lengthString = (Integer.valueOf(bytes.length)).toString(); 16 | var lengthBytes = new ArrayList(); 17 | for (int j = 0; j < lengthString.length(); j++) { 18 | lengthBytes.add((byte)lengthString.charAt(j)); 19 | } 20 | 21 | for (byte b : lengthBytes) { 22 | output.write(b); 23 | } 24 | output.write((byte)':'); 25 | for (byte b : bytes) { 26 | output.write(b); 27 | } 28 | output.write((byte)','); 29 | } 30 | 31 | public static class InvalidNetstringException extends RuntimeException { 32 | static final long serialVersionUID = 0; 33 | InvalidNetstringException() { 34 | super(); 35 | } 36 | InvalidNetstringException(String s) { 37 | super(s); 38 | } 39 | } 40 | 41 | // Given an input stream of bytes, decode one netstring from it, and return 42 | // the resultant array of bytes 43 | public static byte[] decodeFrom(InputStream bytes) 44 | throws IOException, InvalidNetstringException { 45 | synchronized(bytes) { 46 | // Read digits representing the length of the string until a ':' 47 | var lengthBytes = new StringBuilder(); 48 | while (true) { 49 | var thisByte = bytes.read(); 50 | if (thisByte == -1) { // end of stream 51 | throw new EOFException("Malformed netstring, unexpected EOF in length block"); 52 | } 53 | var c = (char)thisByte; 54 | if (Character.isDigit(c)) { 55 | lengthBytes.append(c); 56 | } else if (c == ':') { 57 | // valid separator, signals end of length bytes 58 | break; // exit loop 59 | } else { 60 | throw new InvalidNetstringException("Malformed netstring, missing ':'"); 61 | } 62 | } 63 | 64 | // Parse the length bytes to determine how long the rest of the 65 | // netstring will be 66 | var length = Integer.parseInt(lengthBytes.toString()); 67 | 68 | // Read length-many bytes of output 69 | var result = new byte[length]; 70 | for (int j = 0; j < length; j++) { 71 | int thisByte = bytes.read(); 72 | if (thisByte == -1) { // end of stream 73 | throw new EOFException("Malformed netstring, unexpected EOF in data block"); 74 | } 75 | result[j] = (byte)thisByte; 76 | } 77 | 78 | // Expect a final comma 79 | var thisByte = bytes.read(); 80 | if (thisByte == -1) { // end of stream 81 | throw new EOFException("Malformed netstring, unexpected EOF when expecting trailing comma"); 82 | } 83 | if ((char)thisByte != ',') { 84 | throw new InvalidNetstringException("Malformed netstring, missing ','"); 85 | } 86 | 87 | // Return the decoded netstring 88 | return result; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /file-echo-api/file-echo-api/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE PartialTypeSignatures #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | module Main ( main ) where 6 | 7 | import Data.ByteString (ByteString) 8 | import Data.Version (showVersion) 9 | import Data.Typeable 10 | import qualified Options.Applicative as Opt 11 | 12 | import qualified Argo as Argo 13 | import qualified Argo.Doc as Doc 14 | import Argo.DefaultMain ( customMain, userOptions ) 15 | 16 | import qualified FileEchoServer as FES 17 | import qualified Paths_file_echo_api 18 | 19 | main :: IO () 20 | main = customMain parseServerOptions parseServerOptions parseServerOptions parseServerOptions version description getApp 21 | where 22 | getApp opts = 23 | Argo.mkApp 24 | "file-echo-api" 25 | docs 26 | (Argo.defaultAppOpts Argo.PureState) 27 | (mkInitState $ userOptions opts) 28 | serverMethods 29 | 30 | docs :: [Doc.Block] 31 | docs = 32 | [ Doc.Paragraph [Doc.Text "A sample server that demonstrates filesystem caching."] 33 | , Doc.Section "Datatypes" [Doc.datatype @FES.Ignorable] 34 | ] 35 | 36 | description :: String 37 | description = 38 | "An RPC server for loading and printing files." 39 | 40 | mkInitState :: ServerOptions -> (FilePath -> IO ByteString) -> IO FES.ServerState 41 | mkInitState opts reader = FES.initialState (initialFile opts) reader 42 | 43 | newtype ServerOptions = ServerOptions { initialFile :: Maybe FilePath } 44 | 45 | -- This function parses additional options used by this particular 46 | -- application. The ordinary Argo options are still parsed, and these 47 | -- are appended. 48 | parseServerOptions :: Opt.Parser ServerOptions 49 | parseServerOptions = ServerOptions <$> filename 50 | where 51 | filename = 52 | Opt.optional $ Opt.strOption $ 53 | Opt.long "file" <> 54 | Opt.metavar "FILENAME" <> 55 | Opt.help "Initial file to echo" 56 | 57 | -- Display the version number when the --version option is supplied. 58 | version :: Opt.Parser (a -> a) 59 | version = Opt.simpleVersioner (showVersion Paths_file_echo_api.version) 60 | 61 | serverMethods :: [Argo.AppMethod FES.ServerState] 62 | serverMethods = 63 | [ Argo.command "load" (Doc.Paragraph [Doc.Text "Load a file from disk into memory."]) FES.loadCmd 64 | , Argo.command "clear" (Doc.Paragraph [Doc.Text "Forget the loaded file."]) FES.clearCmd 65 | , Argo.command "prepend" (Doc.Paragraph [Doc.Text "Append a string to the left of the current contents."]) FES.prependCmd 66 | , Argo.command "drop" (Doc.Paragraph [Doc.Text "Drop from the left of the current contents."]) FES.dropCmd 67 | , Argo.command "slow clear" (Doc.Paragraph [Doc.Text "Forgets the loaded file slowly (i.e., char by char)."]) FES.slowClear 68 | , Argo.query "implode" (Doc.Paragraph [Doc.Text "Throw an error immediately."]) FES.implodeCmd 69 | , Argo.query "show" (Doc.Paragraph [Doc.Text "Show a substring of the file."]) FES.showCmd 70 | , Argo.query "ignore" (Doc.Paragraph [Doc.Text "Ignore an ", Doc.Link (Doc.TypeDesc (typeRep (Proxy @FES.Ignorable))) "ignorable value", Doc.Text "."]) FES.ignoreCmd 71 | , Argo.query "sleep query" (Doc.Paragraph [Doc.Text "Sleep for a specified number of microseconds."]) FES.sleepQuery 72 | , Argo.notification "destroy state" (Doc.Paragraph [Doc.Text "Destroy a state in the server."]) FES.destroyState 73 | , Argo.notification "destroy all states" (Doc.Paragraph [Doc.Text "Destroy all states in the server."]) FES.destroyAllStates 74 | , Argo.notification "interrupt" (Doc.Paragraph [Doc.Text "Interrupt all threads in the server."]) FES.interruptAllThreads 75 | , Argo.notification "sleep notification" (Doc.Paragraph [Doc.Text "Sleep for a specified number of microseconds."]) FES.sleepNotification 76 | ] 77 | -------------------------------------------------------------------------------- /argo/src/Argo/Doc/ReST.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE AllowAmbiguousTypes #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | module Argo.Doc.ReST (restructuredText) where 5 | 6 | import Control.Monad.Reader 7 | import Control.Monad.Writer 8 | import Data.Char 9 | import Data.Foldable 10 | import Data.Text (Text) 11 | import qualified Data.Text as T 12 | import Numeric.Natural 13 | 14 | import Argo.Doc 15 | 16 | data ReSTContext = 17 | ReSTContext 18 | { sectionNestingLevel :: Natural 19 | , textIndentLevel :: Natural 20 | } 21 | 22 | 23 | restructuredText :: Block -> Text 24 | restructuredText block = 25 | execWriter $ runReaderT (go block) $ ReSTContext 0 0 26 | where 27 | go :: Block -> ReaderT ReSTContext (Writer Text) () 28 | go (Section name contents) = 29 | do header name 30 | terpri 31 | nestSection (traverse_ go contents) 32 | terpri 33 | go (Datatype t name contents) = 34 | do anchor (mangle (show t)) 35 | header name 36 | terpri 37 | nestSection (traverse_ go contents) 38 | terpri 39 | go (App name contents) = go (Section name contents) 40 | go (Paragraph contents) = 41 | do traverse_ inline contents 42 | terpri >> terpri 43 | go (DescriptionList elts) = 44 | for_ elts $ \(name, what) -> 45 | do terpri 46 | traverse_ inline name 47 | indented $ terpri >> go what 48 | terpri 49 | go (BulletedList elts) = 50 | for_ elts $ 51 | \contents -> 52 | do terpri 53 | tell "* " 54 | indented (go contents) 55 | terpri 56 | 57 | nestSection :: 58 | ReaderT ReSTContext (Writer Text) a -> 59 | ReaderT ReSTContext (Writer Text) a 60 | nestSection = 61 | local $ \(ReSTContext lvl indent) -> ReSTContext (lvl + 1) indent 62 | 63 | indented :: 64 | ReaderT ReSTContext (Writer Text) a -> 65 | ReaderT ReSTContext (Writer Text) a 66 | indented = 67 | local $ \(ReSTContext lvl indent) -> ReSTContext lvl (indent + 2) 68 | 69 | inline :: Inline -> ReaderT ReSTContext (Writer Text) () 70 | inline (Text txt) = traverse_ tell $ T.lines txt 71 | inline (Link tgt label) = 72 | case tgt of 73 | URL url -> tell $ "`" <> label <> " <" <> url <> ">`_" 74 | TypeDesc t -> tell $ ":ref:`" <> label <> " <" <> mangle (show t) <> ">`" 75 | inline (Literal txt) = tell $ "``" <> txt <> "``" 76 | 77 | terpri :: ReaderT ReSTContext (Writer Text) () 78 | terpri = 79 | do i <- asks textIndentLevel 80 | tell "\n" 81 | tell (T.replicate (fromIntegral i) " ") 82 | 83 | anchor :: Text -> ReaderT ReSTContext (Writer Text) () 84 | anchor name = tell $ ".. _" <> name <> ":\n" 85 | 86 | mangle :: String -> Text 87 | mangle [] = "" 88 | mangle (c:cs) 89 | | isAlphaNum c = T.singleton c <> mangle cs 90 | | c == '-' = "_dash_" <> mangle cs 91 | | c == '>' = "_greater_" <> mangle cs 92 | | c == '<' = "_less_" <> mangle cs 93 | | c == '(' = "_lpar_" <> mangle cs 94 | | c == ')' = "_rpar_" <> mangle cs 95 | | c == ' ' = "_space_" <> mangle cs 96 | | c == ':' = "_colon_" <> mangle cs 97 | | c == '~' = "_twiddle_" <> mangle cs 98 | | c == '[' = "_lsq_" <> mangle cs 99 | | c == ']' = "_rsq_" <> mangle cs 100 | | c == '*' = "_splat_" <> mangle cs 101 | | otherwise = "___" <> mangle cs 102 | 103 | 104 | header :: Text -> ReaderT ReSTContext (Writer Text) () 105 | header name = 106 | do tell name 107 | terpri 108 | c <- headerChar 109 | tell $ T.replicate (T.length name) (T.singleton c) 110 | terpri 111 | 112 | headerChar = 113 | do lvl <- asks sectionNestingLevel 114 | pure (getC lvl "=-~+*#`_") 115 | where getC _ [] = error "Nesting too deep in docs" 116 | getC n (c:cs) 117 | | n == 0 = c 118 | | otherwise = getC (n - 1) cs 119 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/Connection.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import com.eclipsesource.json.*; 4 | import java.util.function.*; 5 | import java.util.*; 6 | import java.io.*; 7 | import com.galois.cryptol.client.connection.ConnectionManager.PipeFactory; 8 | 9 | import com.galois.cryptol.client.connection.*; 10 | import com.galois.cryptol.client.connection.json.*; 11 | 12 | public class Connection implements AutoCloseable { 13 | 14 | private volatile JsonValue currentState; 15 | private final JsonConnection jsonConnection; 16 | 17 | public Connection(ProcessBuilder builder, 18 | ConnectionManager.PipeFactory makePipe, 19 | Consumer handleException) 20 | throws IOException { 21 | this(new ConnectionManager(builder, makePipe), handleException); 22 | } 23 | 24 | public Connection(ConnectionManager connectionManager, 25 | Consumer handleException) { 26 | this(new JsonConnection(connectionManager, handleException)); 27 | } 28 | 29 | public Connection(JsonConnection jsonConnection) { 30 | this.jsonConnection = jsonConnection; 31 | this.currentState = null; 32 | } 33 | 34 | public Connection(Connection other) { 35 | this.jsonConnection = other.jsonConnection; 36 | this.currentState = other.currentState; 37 | } 38 | 39 | public O call(JsonRpcCall call) 40 | throws E, ConnectionException { 41 | return jsonConnection.call(new StatefulCall(call)); 42 | } 43 | 44 | public void notify(JsonRpcNotification notification) 45 | throws ConnectionException { 46 | jsonConnection.notify(new StatefulNotification(notification.method(), 47 | notification.params())); 48 | } 49 | 50 | @Override 51 | public void close() throws IOException { 52 | this.jsonConnection.close(); 53 | } 54 | 55 | private class StatefulNotification extends JsonRpcNotification { 56 | 57 | public StatefulNotification(String method, JsonValue params) { 58 | super(method, params); 59 | } 60 | 61 | public JsonValue params() { 62 | try { 63 | JsonObject params = super.params().asObject(); 64 | if (currentState != null) { 65 | params.add("state", currentState); 66 | } 67 | return Json.object().merge(params); 68 | } catch (UnsupportedOperationException e) { 69 | throw new IllegalArgumentException("Stateful call params not an object", e); 70 | } 71 | } 72 | } 73 | 74 | private class StatefulCall extends JsonRpcCall { 75 | 76 | public StatefulCall(JsonRpcCall call) { 77 | // We inherit the special params() behavior from StatefulNotification 78 | super(new StatefulNotification(call.method(), call.params()), 79 | call.decoder, call.handler); 80 | } 81 | 82 | // And then we further override the decode() behavior to set the state 83 | public O decode(JsonValue o) { 84 | try { 85 | JsonObject callResult = o.asObject(); 86 | synchronized(Connection.this) { 87 | var newState = callResult.get("state"); 88 | // Update the current state if there has been an update 89 | currentState = newState != null ? newState : currentState; 90 | } 91 | JsonValue answer = callResult.get("answer"); 92 | if (answer != null) { 93 | return super.decode(answer); 94 | } else { 95 | throw new IllegalArgumentException("No answer field in stateful result"); 96 | } 97 | } catch (UnsupportedOperationException e) { 98 | throw new IllegalArgumentException("Stateful call params not an object", e); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/ManagedPipe.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import java.util.*; 4 | import java.util.function.*; 5 | import java.io.*; 6 | import java.util.concurrent.atomic.*; 7 | 8 | public class ManagedPipe implements Pipe { 9 | 10 | private static int defaultMaxRetries = 1; 11 | 12 | private final ConnectionManager newPipe; 13 | private final Runnable onRetry; 14 | private final int maxRetries; 15 | 16 | private volatile Pipe pipe; 17 | private volatile int remainingRetries; 18 | private volatile int totalRetryCount = 0; 19 | 20 | public ManagedPipe(ConnectionManager newPipe) { 21 | this(newPipe, () -> { }); 22 | } 23 | 24 | public ManagedPipe(ConnectionManager newPipe, Runnable onRetry) { 25 | this(newPipe, onRetry, ManagedPipe.defaultMaxRetries); 26 | } 27 | 28 | public ManagedPipe(ConnectionManager newPipe, 29 | Runnable onRetry, 30 | int maxRetries) { 31 | this.newPipe = newPipe; 32 | this.onRetry = onRetry; 33 | this.maxRetries = maxRetries; 34 | this.remainingRetries = maxRetries; 35 | } 36 | 37 | private void initPipe() throws IOException { 38 | if (pipe == null) { 39 | pipe = newPipe.get(); 40 | } 41 | } 42 | 43 | @Override 44 | public void send(A input) { 45 | try { 46 | initPipe(); 47 | while (true) { 48 | int retryCount = totalRetryCount; 49 | try { 50 | this.pipe.send(input); 51 | remainingRetries = maxRetries; 52 | return; 53 | } catch (Exception e) { 54 | // Only re-initialize pipe if nobody else has in the meantime 55 | synchronized(this) { 56 | if (retryCount == totalRetryCount) { 57 | totalRetryCount++; 58 | if (remainingRetries > 0) { 59 | // System.err.println("\u001B[31m" + "Retrying send after error: " + e + "\u001B[0m"); 60 | this.pipe = this.newPipe.get(); 61 | remainingRetries--; 62 | } else { 63 | throw new ConnectionException(e); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } catch (IOException e) { 70 | throw new ConnectionException(e); 71 | } 72 | } 73 | 74 | public A receive() { 75 | try { 76 | initPipe(); 77 | while (true) { 78 | int retryCount = totalRetryCount; 79 | try { 80 | A result = this.pipe.receive(); 81 | remainingRetries = maxRetries; 82 | return result; 83 | } catch (Exception e) { 84 | synchronized(this) { 85 | if (retryCount == totalRetryCount) { 86 | totalRetryCount++; 87 | if (remainingRetries > 0) { 88 | // System.err.println("\u001B[31m" + "Retrying receive after error: " + e + "\u001B[0m"); 89 | this.pipe = this.newPipe.get(); 90 | remainingRetries--; 91 | } else { 92 | throw new ConnectionException(e); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } catch (IllegalStateException e) { 99 | throw new ConnectionException(e); 100 | } catch (IOException e) { 101 | throw new ConnectionException(e); 102 | } 103 | } 104 | 105 | public void close() throws IOException { 106 | this.newPipe.close(); 107 | this.pipe.close(); 108 | } 109 | 110 | public boolean isClosed() { 111 | return pipe != null ? this.pipe.isClosed() : false; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/queue/ConcurrentMultiQueue.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection.queue; 2 | 3 | import java.util.*; 4 | import java.util.function.*; 5 | import java.util.concurrent.*; 6 | 7 | import com.galois.cryptol.client.connection.queue.*; 8 | 9 | public class ConcurrentMultiQueue implements AutoCloseable { 10 | 11 | // A mapping from channel name to future queue for messages 12 | private final Map> channels; 13 | 14 | // Flag determining whether new messages will be accepted; monotonic 15 | private volatile boolean closed = false; 16 | 17 | public ConcurrentMultiQueue() { 18 | channels = new ConcurrentHashMap>(); 19 | } 20 | 21 | public void send(C channelName, M message) throws QueueClosedException { 22 | channels.compute(channelName, (_k, q) -> { 23 | if (closed) { 24 | throw new QueueClosedException(); 25 | } else { 26 | // Otherwise, open up a new channel if there wasn't one 27 | q = (q != null) ? q : new FutureQueue(); 28 | // Send the message on the channel 29 | q.put(message); // might throw QueueClosedException() 30 | // Determine whether to keep the channel around 31 | if (q.isEmpty()) { 32 | // If the queue is inert now, remove it 33 | // This prevents memory leaks 34 | return null; 35 | } else { 36 | // Otherwise reinsert it 37 | return q; 38 | } 39 | } 40 | }); 41 | } 42 | 43 | public M request(C channelName) throws QueueClosedException { 44 | // We'll communicate the queue's response through this side channel 45 | // The wrapper object hack is necessary to get around the restriction 46 | // that things touched inside lambdas must be "effectively final"; see: 47 | // 48 | var response = new Object() { Future future = null; }; 49 | 50 | // Atomically operate over the channel 51 | channels.compute(channelName, 52 | (_k, q) -> { 53 | // Open up a new channel if there wasn't one 54 | if (q == null) { 55 | q = new FutureQueue(); 56 | // match the closed-ness of everything else 57 | // this prevents a race condition during this.close() 58 | if (closed) q.close(); 59 | } 60 | // Get a response future from the channel 61 | // (and write it out to our side-channel) 62 | response.future = q.takeFuture(); 63 | // Determine whether to keep the channel around -- removing 64 | // empty channels prevents memory leaks when they stop being 65 | // used 66 | if (q.isEmpty()) { 67 | return null; 68 | } else { 69 | return q; 70 | } 71 | }); 72 | 73 | // Wait on the returned future (may throw a CancellationException) 74 | try { 75 | return response.future.get(); 76 | } catch (CancellationException e) { 77 | throw new QueueClosedException(); 78 | // These cases should be impossible because nothing interrupts any of 79 | // the futures within the FutureQueue 80 | } catch (InterruptedException e) { 81 | throw new RuntimeException(e); 82 | } catch (ExecutionException e) { 83 | throw new RuntimeException(e); 84 | } 85 | } 86 | 87 | // Shuts down all channels: all new input from send() throw exceptions, and 88 | // all calls to request() become non-blocking, throwing 89 | // QueueClosedExceptions immediately if there is no data on the channel to 90 | // receive. Blocks until all messages have been removed, either by request() 91 | // or clear(). 92 | public void close() { 93 | closed = true; // no new channels will be formed after this 94 | channels.forEach((_k, q) -> q.close()); // close all existing channels 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docs/Protocol.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Protocol Overview 3 | ================= 4 | 5 | This repository contains servers that implement APIs for interacting 6 | remotely with SAW and Cryptol. These APIs are realized as JSON-RPC_ 7 | protocols, and they can be used either over stdio or a socket. 8 | 9 | .. _JSON-RPC: https://www.jsonrpc.org/specification 10 | 11 | Both protocols are built on a common foundation. The specific actions 12 | available for SAW or Cryptol are, however, different. This document is 13 | divided into three main sections: one about the common protocol, one 14 | about the Cryptol-specific interaction commands, and one about the 15 | SAW-specific commands. 16 | 17 | Overall Protocol Structure 18 | ========================== 19 | 20 | The interaction protocols are designed primarily to support 21 | interactive, rather than batch mode, use of SAW. SAW is a stateful 22 | system: LLVM, JVM, and Cryptol modules are loaded, and then proofs are 23 | carried out in the resulting context. Each step can consume 24 | significant time and computing resources, so it is best to avoid 25 | needless repetition of steps. Additionally, a step might fail, and 26 | recovering from that failure may require backtracking an arbitrary 27 | number of steps. 28 | 29 | To minimize the necessary amount of recomputation when backtracking 30 | and exploring alternative proof strategies, the server performs 31 | extensive caching. When a client issues a command, they additionally 32 | provide a representation of the state in which the command is to be 33 | performed; when the server has performed the command, it returns a new 34 | state along with the result. Clients may re-use server states as many 35 | times as they desire. 36 | 37 | Explicitly representing the state in the protocol has additional 38 | benefits. The server can be restarted at any time, with the worst 39 | consequence being a delay in its response. The client can maintain its 40 | own handle into the server state, persist it to disk, and pick up 41 | where it left off on another computer. 42 | 43 | Transport 44 | --------- 45 | 46 | The JSON-RPC messages in the protocol are encoded in netstrings_, to 47 | make it easier to allocate buffers ahead of time and to avoid bugs 48 | with missing terminator characters. 49 | 50 | .. _netstrings: http://cr.yp.to/proto/netstrings.txt 51 | 52 | The servers support two modes of operation: stdio and sockets. 53 | 54 | State 55 | ----- 56 | 57 | According to the JSON-RPC specification, the ``params`` field in a 58 | message object must be an array or object. In this protocol, it is 59 | always an object. While each message may specify its own arguments, 60 | every message has a parameter field named ``state``. 61 | 62 | When the first message is sent from the client to the server, the 63 | ``state`` parameter should be initialized to the JSON null value 64 | ``null``. Replies from the server may contain a new state that should 65 | be used in subsequent requests, so that state changes executed by the 66 | request are visible. Prior versions of this protocol represented the 67 | initial state as the empty array ``[]``, but this is now deprecated 68 | and will be removed. 69 | 70 | In particular, per JSON-RPC, non-error replies are always a JSON 71 | object that contains a ``result`` field. The result field always 72 | contains an ``answer`` field and a ``state`` field, as well as 73 | ``stdout`` and ``stderr``. 74 | 75 | ``answer`` 76 | The value returned as a response to the request (the precise 77 | contents depend on which request was sent) 78 | 79 | ``state`` 80 | The state, to be sent in subsequent requests. If the server did not 81 | modify its state in response to the command, then this state may be 82 | the same as the one sent by the client. 83 | 84 | ``stdout`` and ``stderr`` 85 | These fields contain the contents of the Unix ``stdout`` and 86 | ``stderr`` file descriptors. They are intended as a stopgap measure 87 | for clients who are still in the process of obtaining structured 88 | information from the libraries on which they depend, so that 89 | information is not completely lost to users. However, the server may 90 | or may not cache this information and resend it. Applications are 91 | encouraged to used structured data and send it deliberately as the answer. 92 | 93 | The precise structure of states is considered an implementation detail 94 | that could change at any time. Please treat them as opaque tokens that 95 | may be saved and re-used within a given server process, but not 96 | created by the client directly. 97 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/CryptolConnection.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client; 2 | 3 | import java.util.*; 4 | import java.util.function.*; 5 | import java.io.*; 6 | import java.net.*; 7 | 8 | import com.eclipsesource.json.*; 9 | 10 | import com.galois.cryptol.client.*; 11 | import com.galois.cryptol.client.connection.*; 12 | import com.galois.cryptol.client.connection.json.*; 13 | import com.galois.cryptol.client.connection.netstring.*; 14 | 15 | public class CryptolConnection implements AutoCloseable { 16 | 17 | private final Connection connection; 18 | 19 | private static void forLinesAsync(InputStream i, Consumer c) { 20 | (new Thread(() -> { 21 | try { 22 | (new BufferedReader(new InputStreamReader(i))) 23 | .lines().forEach(c); 24 | } catch (Exception e) { 25 | // Do nothing; the stream is gone, for some reason 26 | } 27 | })).start(); 28 | } 29 | 30 | public CryptolConnection(String server, File dir) throws IOException { 31 | this.connection = 32 | new Connection( 33 | new ProcessBuilder(server, "--dynamic4").directory(dir), 34 | (_in, out, err) -> { 35 | // The process will tell us what port to connect to... 36 | int port = (new Scanner(out)).skip("PORT ").nextInt(); 37 | // Consume the remaining output and error 38 | forLinesAsync(out, l -> { }); 39 | // Output debug messages from the subprocess to stderr 40 | // (the ANSI escape codes mean "foreground = red") 41 | forLinesAsync(err, l -> System.err.println("\u001B[31m" + l + "\u001B[0m")); 42 | // Connect to the port 43 | var s = new Socket("127.0.0.1", port); 44 | var socketIn = s.getInputStream(); 45 | var socketOut = s.getOutputStream(); 46 | // Make a JSON-netstring layer across the connection 47 | return new JsonPipe(new NetstringPipe(socketIn, socketOut)); 48 | }, 49 | e -> { 50 | System.err.println("Connection error:"); 51 | e.printStackTrace(); 52 | }); 53 | } 54 | 55 | // Close the connection 56 | public synchronized void close() throws IOException { 57 | connection.close(); 58 | } 59 | 60 | // Since this runs on actual output/input streams, we know that connection 61 | // exceptions in this case are really IOExceptions, so we use this wrapper 62 | // to allow the caller to not need to see ConnectionExceptions 63 | private O callMethod(String method, JsonValue params, 64 | Function decode) 65 | throws CryptolException { 66 | JsonRpcCall call = 67 | new JsonRpcCall<>(method, params, decode, error -> { 68 | // Ensure the error is in range for Cryptol 69 | if (error.code < 20000 || error.code > 21000) { 70 | return null; 71 | } 72 | final int code = error.code - 20000; // error code 73 | final String path; // maybe there's an associated path 74 | { 75 | var p = error.data.asObject().get("path"); 76 | if (p != null) { 77 | path = p.asString(); 78 | } else { 79 | path = null; 80 | } 81 | } 82 | return new CryptolException(error.message); 83 | }); 84 | return connection.call(call); 85 | } 86 | 87 | // The calls available: 88 | 89 | public void loadModule(String file) throws CryptolException { 90 | this.callMethod("load module", 91 | Json.object().add("file", file), 92 | v -> new Unit()); 93 | } 94 | 95 | public String evalExpr(String expr) throws CryptolException { 96 | return this.callMethod("evaluate expression", 97 | Json.object().add("expression", expr), 98 | v -> v.asObject().get("value").toString()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /argo/src/Argo/Netstring.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE MultiWayIf #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | 5 | -- | A netstring is an ASCII-encoded sequence of digits representing a 6 | -- number N, followed by an ASCII colon, followed by N bytes, followed 7 | -- by an ASCII comma. This allows receivers to know how much is needed. 8 | -- 9 | -- Definition: http://cr.yp.to/proto/netstrings.txt 10 | -- 11 | -- Netstrings allow malformed JSON to be more robustly detected when 12 | -- using JSON-RPC. 13 | module Argo.Netstring 14 | ( Netstring 15 | , encodeNetstring 16 | , decodeNetstring 17 | , netstring 18 | , parseNetstring 19 | , netstringFromHandle 20 | ) 21 | where 22 | 23 | import Control.Exception 24 | import Data.ByteString.Lazy (ByteString) 25 | import qualified Data.ByteString.Lazy as BS 26 | import qualified Data.ByteString.Builder as BS 27 | 28 | import Data.Word 29 | import System.IO (Handle, hIsEOF) 30 | 31 | data BadNetstring 32 | = BadLength 33 | | MissingColon (Maybe Word8) 34 | | MissingComma (Maybe Word8) 35 | deriving Show 36 | 37 | instance Exception BadNetstring 38 | 39 | -- | A ByteString together with a header that provides its length. 40 | newtype Netstring 41 | = Netstring ByteString 42 | deriving (Eq, Ord) 43 | 44 | instance Show Netstring where 45 | show (Netstring s) = "(netstring " ++ show s ++ ")" 46 | 47 | -- | Construct a new netstring from a bytestring 48 | netstring :: ByteString -> Netstring 49 | netstring = Netstring 50 | 51 | -- | Get the underlying bytestring in a netstring 52 | decodeNetstring :: Netstring -> ByteString 53 | decodeNetstring (Netstring s) = s 54 | 55 | -- | Encode a netstring as a bytestring, which will contain its length 56 | encodeNetstring :: Netstring -> ByteString 57 | encodeNetstring (Netstring bytes) = 58 | BS.toLazyByteString $ 59 | BS.stringUtf8 (show (BS.length bytes)) <> 60 | BS.charUtf8 ':' <> 61 | BS.lazyByteString bytes <> 62 | BS.charUtf8 ',' 63 | 64 | -- >>> toNetstring "hello" 65 | -- "5:hello," 66 | 67 | 68 | -- | Read a netstring from a handle. Return Nothing on end of file. 69 | netstringFromHandle :: Handle -> IO (Maybe Netstring) 70 | netstringFromHandle h = 71 | do eof <- hIsEOF h 72 | if eof 73 | then return Nothing 74 | else Just <$> getNetString 75 | where 76 | getNetString = 77 | do l <- len Nothing 78 | bytes <- BS.hGet h l 79 | c <- BS.head <$> BS.hGet h 1 80 | if isComma c 81 | then return (Netstring bytes) 82 | else throwIO (MissingComma (Just c)) 83 | 84 | len Nothing = 85 | do x <- BS.head <$> BS.hGet h 1 86 | if not (isDigit x) 87 | then throwIO BadLength 88 | else len (Just (asDigit x)) 89 | 90 | len (Just acc) = 91 | do x <- BS.head <$> BS.hGet h 1 92 | if | isColon x -> return acc 93 | | isDigit x -> len (Just (10 * acc + asDigit x)) 94 | | otherwise -> throwIO (MissingColon (Just x)) 95 | 96 | -- | Attempt to split a ByteString into a prefix that is a valid netstring, and an arbitrary suffix 97 | parseNetstring :: ByteString -> (Netstring, ByteString) 98 | parseNetstring input = 99 | let (lenBytes, rest) = BS.span isDigit input 100 | len = asLength lenBytes 101 | in 102 | case BS.uncons rest of 103 | Nothing -> throw (MissingColon Nothing) 104 | Just (c, rest') 105 | | isColon c -> 106 | let (content, rest'') = BS.splitAt (fromIntegral len) rest' 107 | in 108 | case BS.uncons rest'' of 109 | Nothing -> throw (MissingComma Nothing) 110 | Just (b, done) | isComma b -> (netstring content, done) 111 | | otherwise -> throw (MissingComma (Just b)) 112 | | otherwise -> throw (MissingColon (Just c)) 113 | 114 | isDigit :: Word8 -> Bool 115 | isDigit w = w >= 0x30 && w <= 0x39 116 | 117 | isColon :: Word8 -> Bool 118 | isColon w = w == 0x3a 119 | 120 | isComma :: Word8 -> Bool 121 | isComma w = w == 0x2c 122 | 123 | -- | Assumes isDigit of argument 124 | asDigit :: Word8 -> Int 125 | asDigit w = fromIntegral (w - 0x30) 126 | 127 | asLength :: ByteString -> Int 128 | asLength len = 129 | if BS.null len 130 | then throw BadLength 131 | else go 0 (map asDigit (BS.unpack len)) 132 | where 133 | go acc [] = acc 134 | go acc (d:ds) = go (acc * 10 + d) ds 135 | 136 | -- >>> :set -XOverloadedStrings 137 | -- >>> parseNetstring "5:hello,aldskf" 138 | -- ("hello","aldskf") 139 | -------------------------------------------------------------------------------- /argo/src/Argo/Socket.hs: -------------------------------------------------------------------------------- 1 | {-# Language OverloadedStrings #-} 2 | {-# Language ScopedTypeVariables #-} 3 | module Argo.Socket 4 | ( serveSocket 5 | , serveSocketDynamic 6 | ) where 7 | 8 | import Control.Concurrent (forkFinally) 9 | import Control.Concurrent.Async (Async, async, forConcurrently_) 10 | import Control.Exception (displayException) 11 | import Control.Monad (forever) 12 | import qualified Data.Text as T 13 | import System.IO (IOMode(ReadWriteMode), hClose) 14 | 15 | import qualified Network.Socket as N 16 | 17 | import Argo (App, MethodOptions(..), serveHandlesNS) 18 | 19 | -- | Arbitrarily chosen value for number of connects to hold 20 | -- in queue during a burst of connections before they are 21 | -- accepted by the server. 22 | listenQueueDepth :: Int 23 | listenQueueDepth = 10 24 | 25 | -- | Start listening on the given address and serve the given RPC 26 | -- application for all connecting clients. Messages are expected 27 | -- to use netstring-wrapped JSON RPC format. 28 | -- 29 | -- A reasonable default host name is "::", and a reasonable default 30 | -- service name is a port number as a string, e.g. "10000". 31 | serveSocket :: 32 | MethodOptions {- ^ options for how to execute methods -} -> 33 | N.HostName {- ^ host -} -> 34 | N.ServiceName {- ^ port -} -> 35 | App s {- ^ rpc application -} -> 36 | IO () {- ^ start application -} 37 | serveSocket opts hostName serviceName app = 38 | 39 | -- resolve listener addresses, throws exception on failure 40 | do (infos :: [N.AddrInfo]) <- 41 | N.getAddrInfo (Just hints) (Just hostName) (Just serviceName) 42 | 43 | -- open listener sockets on all matched addresses, typically 44 | -- one per address family. 45 | forConcurrently_ infos $ \info -> 46 | do s <- startListening info 47 | forever (acceptClient opts app s) 48 | 49 | -- | Start listening on a single, dynamically assigned port. 50 | -- The resulting worker thread and dynamically assigned port 51 | -- number are returned on success. 52 | serveSocketDynamic :: 53 | MethodOptions {- ^ options for how to execute methods -} -> 54 | N.HostName {- ^ IP address -} -> 55 | App s {- ^ RPC application -} -> 56 | IO (Async (), N.PortNumber) 57 | serveSocketDynamic opts hostName app = 58 | 59 | -- resolve listener addresses, throws exception on failure 60 | do let hint1 = 61 | N.defaultHints 62 | { N.addrFlags = [N.AI_NUMERICHOST, N.AI_ADDRCONFIG] 63 | , N.addrSocketType = N.Stream } 64 | infos <- N.getAddrInfo (Just hint1) (Just hostName) Nothing 65 | info <- case infos of 66 | [info] -> return info 67 | _ -> fail "serveSocketDynamic: host resolved as too many addresses" 68 | 69 | s <- startListening info 70 | a <- async (forever (acceptClient opts app s)) 71 | p <- N.socketPort s 72 | return (a, p) 73 | 74 | 75 | -- | Create a new listening socket for this address. 76 | startListening :: N.AddrInfo -> IO N.Socket 77 | startListening addr = 78 | do s <- N.socket (N.addrFamily addr) 79 | (N.addrSocketType addr) 80 | (N.addrProtocol addr) 81 | N.setSocketOption s N.ReuseAddr 1 -- easier to restart server 82 | N.bind s (N.addrAddress addr) 83 | N.listen s listenQueueDepth 84 | return s 85 | 86 | 87 | -- | Accept a new connection on the given listening socket and 88 | -- start processing rpc requests. 89 | acceptClient :: MethodOptions -> App s -> N.Socket -> IO () 90 | acceptClient opts app s = 91 | 92 | do (c, peer) <- N.accept s 93 | h <- N.socketToHandle c ReadWriteMode 94 | let logMessage = optLogger opts 95 | -- don't use c after this, it is owned by h 96 | 97 | logMessage (T.pack ("CONNECT: " ++ show peer)) 98 | _ <- forkFinally (serveHandlesNS opts h h app) $ \res -> 99 | do case res of 100 | Right _ -> logMessage (T.pack ("CLOSE: " ++ show peer)) 101 | Left e -> logMessage (T.pack ("ERROR: " ++ show peer ++ " " ++ displayException e)) 102 | hClose h 103 | 104 | return () 105 | 106 | 107 | -- | Hints used by 'serveSocket' specifying a stream socket intended for 108 | -- listening for new connections. 109 | hints :: N.AddrInfo 110 | hints = 111 | N.defaultHints 112 | { N.addrFlags = [N.AI_PASSIVE, N.AI_ADDRCONFIG] 113 | , N.addrSocketType = N.Stream -- TCP 114 | } 115 | -------------------------------------------------------------------------------- /argo/src/Argo/Doc/Protocol.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Argo.Doc.Protocol (protocolDocs) where 3 | 4 | import Data.List.NonEmpty (NonEmpty(..)) 5 | 6 | import Argo.Doc 7 | 8 | 9 | -- | Documentation for the details shared by all servers that use this library. 10 | protocolDocs :: Block 11 | protocolDocs = 12 | Section "Fundamental Protocol" $ 13 | [ Paragraph 14 | [ Text "This application is a " 15 | , Link (URL "https://www.jsonrpc.org/specification") "JSON-RPC" 16 | , Text $ 17 | " server. Additionally, it maintains a persistent cache " <> 18 | "of application states and explicitly indicates the state " <> 19 | "in which each command is to be carried out."] 20 | , Section "Transport" 21 | [ Paragraph [Text "The server supports three transport methods:"] 22 | , DescriptionList 23 | [ ( Literal "stdio" :| [] 24 | , Paragraph [ Text "in which the server communicates over " 25 | , Literal "stdin", Text " and ", Literal "stdout" 26 | , Text " using ", Link (URL "http://cr.yp.to/proto/netstrings.txt") "netstrings."] 27 | ) 28 | , ( Literal "socket" :| [] 29 | , Paragraph [ Text "in which the server communicates over a socket using " 30 | , Link (URL "http://cr.yp.to/proto/netstrings.txt") "netstrings." 31 | ] 32 | ) 33 | , ( Literal "http" :| [] 34 | , Paragraph [Text "in which the server communicates over a socket using HTTP."] 35 | ) 36 | ] 37 | ] 38 | , Section "Application State" 39 | [ Paragraph 40 | [ Text "According to the JSON-RPC specification, the ", Literal "params", Text " field in a " 41 | , Text "message object must be an array or object. In this protocol, it is " 42 | , Text "always an object. While each message may specify its own arguments, " 43 | , Text "every message has a parameter field named ", Literal "state", Text "."] 44 | , Paragraph 45 | [ Text "When the first message is sent from the client to the server, the " 46 | , Literal "state", Text " parameter should be initialized to the JSON null value " 47 | , Literal "null", Text ". Replies from the server may contain a new state that should " 48 | , Text "be used in subsequent requests, so that state changes executed by the " 49 | , Text "request are visible." 50 | ] 51 | , Paragraph 52 | [ Text "In particular, per JSON-RPC, non-error replies are always a JSON " 53 | , Text "object that contains a ", Literal "result", Text " field. The result field always " 54 | , Text "contains an ", Literal "answer", Text " field and a ", Literal "state" 55 | , Text " field, as well as ", Literal "stdout", Text " and ", Literal "stderr", Text "." 56 | ] 57 | , DescriptionList 58 | [ (k, Paragraph v) 59 | | (k, v) <- 60 | [ ( Literal "answer" :| [] 61 | , [ Text "The value returned as a response to the request " 62 | , Text "(the precise contents depend on which request was sent)." 63 | ] 64 | ) 65 | , ( Literal "state" :| [] 66 | , [ Text "The state, to be sent in subsequent requests. If the server did not " 67 | , Text "modify its state in response to the command, then this state may be " 68 | , Text "the same as the one sent by the client. When a new state is in a server response" 69 | , Text ", the previous state may no longer be available for requests." 70 | ] 71 | ) 72 | , ( Literal "stdout" :| [Text " and ", Literal "stderr"] 73 | , [ Text "These fields contain the contents of the Unix ", Literal "stdout", Text " and " 74 | , Literal "stderr", Text " file descriptors. They are intended as a stopgap measure " 75 | , Text "for clients who are still in the process of obtaining structured " 76 | , Text "information from the libraries on which they depend, so that " 77 | , Text "information is not completely lost to users. However, the server may " 78 | , Text "or may not cache this information and resend it. Applications are " 79 | , Text "encouraged to used structured data and send it deliberately as the answer." 80 | ] 81 | ) 82 | ] 83 | ] 84 | , Paragraph 85 | [ Text "The precise structure of states is considered an implementation detail " 86 | , Text "that could change at any time. Please treat them as opaque tokens that " 87 | , Text "may be saved and re-used within a given server process, but not " 88 | , Text "created by the client directly." 89 | ] 90 | ] 91 | ] 92 | -------------------------------------------------------------------------------- /protocol-notes.org: -------------------------------------------------------------------------------- 1 | * Protocol 2 | 3 | - Start with Cryptol part of API 4 | - Replace existing =cryptol-server= binary, raiding it for useful 5 | functionality 6 | - We want to support at least what the Cryptol REPL supports, plus 7 | some SAW things. 8 | 9 | * Ideas 10 | 11 | 12 | * States 13 | 14 | A state is represented as a JSON array of the sequence of commands 15 | that gives rise to it. Each command will reply with a state; however, 16 | this state may be a minimized version, where commands that did not 17 | have an effect have been stripped out or where a sequence of commands 18 | that modify the state have been coalesced into a single command. 19 | 20 | The state passed by a client should always have been constructed by 21 | the server, not directly by the client. 22 | 23 | * Application datatypes 24 | 25 | These are datatypes used in the protocol, along with their JSON encodings. 26 | 27 | ** Input datatypes 28 | *** Cryptol expressions 29 | 30 | A Cryptol expression is one of the following: 31 | - A string containing concrete Cryptol syntax 32 | 33 | - A JSON object that represents a handle to an existing expression, 34 | previously sent from the server. These should be treated as opaque 35 | tokens. 36 | 37 | - An object with the following keys: 38 | - =type= :: a Cryptol type, which must describe a finite bit 39 | sequence (e.g. not a function, not an infinite stream) 40 | - =encoding= :: the literal string ="base64"= 41 | - =data= :: A base64-encoded bitstring 42 | 43 | *** Cryptol types 44 | 45 | A Cryptol type is one of the following: 46 | - A string containing concrete Cryptol type syntax 47 | 48 | - A JSON object that represents a handle to an existing type, 49 | previously sent from the server. These should be treated as opaque 50 | tokens. 51 | 52 | ** Output datatypes 53 | 54 | *** Cryptol types 55 | 56 | An output Cryptol type consists of an object with at least the 57 | following fields: 58 | - ="line"= :: the concrete syntax of the type, on one line 59 | - ="formatted"= :: a pretty-printed version of the type 60 | - ="handle"= :: an abstract token representing the type 61 | 62 | 63 | * Protocol Commands 64 | 65 | ** Load module 66 | 67 | - Method :: ="load module"= 68 | - Params :: An object with keys: 69 | - ="file"= :: A path 70 | - ="state"= :: A state representation 71 | 72 | * Cryptol commands 73 | 74 | All commands that are specific to Cryptol 75 | 76 | Each command from the Cryptol REPL, except for =:e= and =:!=, should be part of 77 | the protocol. Here's the output of =:help=: 78 | 79 | #+BEGIN_EXAMPLE 80 | :t, :type check the type of an expression 81 | :b, :browse display the current environment 82 | :?, :help display a brief description of a function or a type 83 | :s, :set set an environmental option (:set on its own displays current values) 84 | :check use random testing to check that the argument always returns true (if no argument, check all properties) 85 | :exhaust use exhaustive testing to prove that the argument always returns true (if no argument, check all properties) 86 | :prove use an external solver to prove that the argument always returns true (if no argument, check all properties) 87 | :sat use a solver to find a satisfying assignment for which the argument returns true (if no argument, find an assignment for all properties) 88 | :debug_specialize do type specialization on a closed expression 89 | :eval evaluate an expression with the reference evaluator 90 | :ast print out the pre-typechecked AST of a given term 91 | :extract-coq print out the post-typechecked AST of all currently defined terms, in a Coq parseable format 92 | :q, :quit exit the REPL 93 | :l, :load load a module 94 | :r, :reload reload the currently loaded module 95 | :e, :edit edit the currently loaded module 96 | :! execute a command in the shell 97 | :cd set the current working directory 98 | :m, :module load a module 99 | :w, :writeByteArray write data of type `fin n => [n][8]` to a file 100 | :readByteArray read data from a file as type `fin n => [n][8]`, binding the value to variable `it` 101 | #+END_EXAMPLE 102 | 103 | In the following list, keys refer to method names, and values are the 104 | contents of the =params= object. In addition to the listed fields, 105 | every =params= object should additionally have the field =state=, 106 | giving the state in which the command should execute. 107 | 108 | - =cryptol type= :: Check the type of an expression. 109 | - ="params"= :: 110 | - =expression= :: A Cryptol expression 111 | - Result: A Cryptol type 112 | 113 | 114 | 115 | ** Cryptol error messages 116 | 117 | * SAW commands 118 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Argo: A JSON-RPC Server Library 2 | =============================== 3 | 4 | This repository contains ``argo``, a library designed for adding 5 | JSON-RPC support to existing applications. Features include: 6 | 7 | * Explicit management of application state, enabling low-cost 8 | rollbacks to earlier states and multiple independent clients. 9 | 10 | * Support for socket, ``stdio``, and HTTP transports, all in one server. 11 | 12 | * Caching of application states, including filesystem state. 13 | 14 | * Python bindings 15 | 16 | Users 17 | ----- 18 | 19 | This library is currently used to provide JSON-RPC interfaces to 20 | `Cryptol `_ 21 | and `SAW `_. 22 | 23 | 24 | Build Instructions 25 | ------------------ 26 | 27 | This repository contains: 28 | 29 | * The ``argo`` library, in Haskell 30 | 31 | * Python bindings for ``argo``, as well as for the Cryptol and SAW clients 32 | 33 | * Sphinx documentation for the above 34 | 35 | * A sample server, in the ``file-echo-api`` directory. This server 36 | demonstrates the use of ``argo`` and is useful for testing. 37 | 38 | * An extension to ``tasty`` for invoking scripts written in a language 39 | other than Haskell (in this case, Python). This is in the 40 | ``tasty-script-exitcode`` directory. 41 | 42 | Argo is primarily intended to be used as a dependency of another 43 | server. To build and test it on its own, use:: 44 | 45 | cabal v2-build 46 | cabal v2-test argo file-echo-api 47 | 48 | Build tools 49 | ~~~~~~~~~~~ 50 | 51 | Requirements: 52 | 53 | * cabal-install 2.4.1.0 or newer 54 | * GHC-8.10.7 or newer 55 | * Python 3.7 or higher 56 | 57 | Any easy to way get GHC and cabal-install installed is to use `ghcup`_; 58 | however any other method will be fine:: 59 | 60 | $ ghcup install 9.2.8 61 | $ ghcup set 9.2.8 # optional 62 | $ ghcup install-cabal 63 | 64 | .. _ghcup: https://gitlab.haskell.org/haskell/ghcup 65 | 66 | 67 | Documentation 68 | ------------- 69 | 70 | The protocol and the Python bindings are described in Sphinx-buildable 71 | ReStructuredText format in the [docs](docs/) subdirectory. Use ``make html`` 72 | in that directory to build readable HTML output. 73 | 74 | Python 75 | ~~~~~~ 76 | 77 | The real goal of this system is to support Python bindings to SAW. The 78 | Python bindings are in the ``python`` subdirectory. Right now, the 79 | Cryptol support is more advanced, but the SAW support is under 80 | development. The bindings are tested only with Python 3.7 and newer. 81 | 82 | To install the Python bindings, we recommend the use of a "virtual 83 | environment" that isolates collections of Python packages that are 84 | used for different projects. To create a virtual environment, use the 85 | command:: 86 | 87 | python3 -m venv virtenv 88 | 89 | The preferred mode of use for virtual environments is to *activate* 90 | them, which modifies various environment variables to cause the 91 | current shell's view of the Python implementations, tools, and 92 | libraries to match the environment. For instance, ``PATH`` is modified 93 | to prioritize the virtual environment's Python version, and that 94 | Python is pointed at the specific collection of libraries that are 95 | available. Under a broadly Bourne-compatible shell like ``bash`` or 96 | ``zsh``, source the appropriate file in the environment:: 97 | 98 | . virtenv/bin/activate 99 | 100 | to activate the environment. Deactivate it using the shell alias 101 | ``deactivate`` that is defined by ``activate``. For other shells or 102 | operating systems, please consult the documentation for ``venv``. If 103 | you prefer not to activate the environment, it is also possible to run 104 | the environment's version of Python tooling by invoking the scripts in 105 | its ``bin`` directory. 106 | 107 | In the virtual environment, run the following command to install the 108 | library's dependencies:: 109 | 110 | pip install -r python/requirements.txt 111 | 112 | Next, install the library itself:: 113 | 114 | pip install -e python/ 115 | 116 | The ``-e`` flag to ``pip install`` causes it to use the current files 117 | in the repository as the library's source rather than copying them to 118 | a central location in the virtual environment. This means that they 119 | can be edited in-place and tested immediately, with no reinstallation 120 | step. If you'd prefer to just install them, then omit the ``-e`` flag. 121 | 122 | Working on the Python bindings 123 | ============================== 124 | 125 | To run the ``mypy`` type checker, enter the virtual environment and then run:: 126 | 127 | mypy argo_client 128 | 129 | from the ``python`` subdirectory. 130 | 131 | Actually using the application-specific bindings requires the 132 | appropriate server (please refer to the links at the beginning of this 133 | document). 134 | 135 | -------------------------------------------------------------------------------- /java/src/test/java/com/galois/cryptol/client/MultiQueueDemo.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client; 2 | 3 | import java.util.*; 4 | import java.io.*; 5 | import java.net.*; 6 | import java.util.concurrent.*; 7 | import java.util.function.*; 8 | 9 | import com.eclipsesource.json.*; 10 | import com.galois.cryptol.client.*; 11 | import com.galois.cryptol.client.connection.queue.*; 12 | 13 | class MultiQueueDemo { 14 | 15 | public static void main(String[] args) { 16 | if (args.length == 3) { 17 | multiQueueDemo(Integer.parseInt(args[0]), 18 | Double.parseDouble(args[1]), 19 | Integer.parseInt(args[2])); 20 | } else { 21 | System.err.println("Wrong number of arguments: please specify (channels, mean delay, timeout)"); 22 | System.exit(1); 23 | } 24 | } 25 | 26 | // Visual demo of concurrent keyed channel: run a receiving and a sending 27 | // thread per channel, each with random delay between send() and request() 28 | // calls, displaying the method calls in a table. The simulation lasts for 29 | // the timeout parameter, in seconds 30 | public static void multiQueueDemo(int channelCount, double meanDelay, int timeout) { 31 | // Channels 32 | var channels = new ConcurrentMultiQueue(); 33 | 34 | // Sending threads 35 | var sending = new ArrayList(); 36 | for (int c = 0; c < channelCount; c++) { 37 | int channel = c; 38 | sending.add(() -> { 39 | int message = 0; 40 | while (true) { 41 | long wait = (long)(2 * 1000 * meanDelay * Math.random()); 42 | try { 43 | TimeUnit.MILLISECONDS.sleep(wait); 44 | } catch (InterruptedException e) { 45 | throw new RuntimeException(e); 46 | } 47 | try { 48 | channels.send(channel, message); 49 | } catch (QueueClosedException e) { 50 | break; 51 | } 52 | synchronized(System.out) { 53 | for (int i = 0; i < channel; i++) System.out.print("\t\t\t\t"); 54 | System.out.println(channel + ": SEND " + message); 55 | } 56 | message++; 57 | } 58 | synchronized(System.out) { 59 | for (int i = 0; i < channel; i++) System.out.print("\t\t\t\t"); 60 | System.out.println(channel + ": STOPPED"); 61 | } 62 | }); 63 | } 64 | 65 | // Receiving threads 66 | var receiving = new ArrayList(); 67 | for (int c = 0; c < channelCount; c++) { 68 | int channel = c; 69 | receiving.add(() -> { 70 | while (true) { 71 | long wait = (long)(2 * 1000 * meanDelay * Math.random()); 72 | try { 73 | TimeUnit.MILLISECONDS.sleep(wait); 74 | } catch (InterruptedException e) { 75 | throw new RuntimeException(e); 76 | } 77 | synchronized(System.out) { 78 | for (int i = 0; i < channel; i++) System.out.print("\t\t\t\t"); 79 | System.out.println("\t\t" + channel + ": REQUEST "); 80 | } 81 | Integer message; 82 | try { 83 | message = channels.request(channel); 84 | } catch (QueueClosedException e) { 85 | break; 86 | } 87 | synchronized(System.out) { 88 | for (int i = 0; i < channel; i++) System.out.print("\t\t\t\t"); 89 | System.out.println("\t\t" + channel + ": RECEIVE " + message); 90 | } 91 | } 92 | synchronized(System.out) { 93 | for (int i = 0; i < channel; i++) System.out.print("\t\t\t\t"); 94 | System.out.println("\t\t" + channel + ": CANCELLED "); 95 | } 96 | }); 97 | } 98 | 99 | // Start all threads 100 | System.out.println(); 101 | for (var f : sending) (new Thread(f)).start(); 102 | for (var f : receiving) (new Thread(f)).start(); 103 | 104 | (new Thread(() -> { 105 | try { 106 | TimeUnit.SECONDS.sleep(timeout); 107 | synchronized(System.out) { 108 | channels.close(); 109 | for (int i = 0; i < channelCount; i++) { 110 | System.out.print("--------------------------------"); 111 | } 112 | System.out.println(); 113 | } 114 | channels.close(); 115 | synchronized(System.out) { 116 | channels.close(); 117 | for (int i = 0; i < channelCount; i++) { 118 | System.out.print("--------------------------------"); 119 | } 120 | System.out.println(); 121 | } 122 | } catch (InterruptedException e) { 123 | } 124 | })).start(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /java/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../python')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'Argo' 23 | copyright = '2019, Galois, Inc.' 24 | author = 'Galois, Inc.' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.doctest', 44 | 'sphinx.ext.coverage', 45 | 'sphinx.ext.mathjax', 46 | 'sphinx.ext.viewcode', 47 | ] 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ['_templates'] 51 | 52 | # The suffix(es) of source filenames. 53 | # You can specify multiple suffix as a list of string: 54 | # 55 | # source_suffix = ['.rst', '.md'] 56 | source_suffix = '.rst' 57 | 58 | # The master toctree document. 59 | master_doc = 'index' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This pattern also affects html_static_path and html_extra_path. 71 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = None 75 | 76 | 77 | # -- Options for HTML output ------------------------------------------------- 78 | 79 | # The theme to use for HTML and HTML Help pages. See the documentation for 80 | # a list of builtin themes. 81 | # 82 | html_theme = 'nature' 83 | 84 | # Theme options are theme-specific and customize the look and feel of a theme 85 | # further. For a list of options available for each theme, see the 86 | # documentation. 87 | # 88 | # html_theme_options = {} 89 | 90 | # Add any paths that contain custom static files (such as style sheets) here, 91 | # relative to this directory. They are copied after the builtin static files, 92 | # so a file named "default.css" will overwrite the builtin "default.css". 93 | html_static_path = ['_static'] 94 | 95 | # Custom sidebar templates, must be a dictionary that maps document names 96 | # to template names. 97 | # 98 | # The default sidebars (for documents that don't match any pattern) are 99 | # defined by theme itself. Builtin themes are using these templates by 100 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 101 | # 'searchbox.html']``. 102 | # 103 | # html_sidebars = {} 104 | 105 | 106 | # -- Options for HTMLHelp output --------------------------------------------- 107 | 108 | # Output file base name for HTML help builder. 109 | htmlhelp_basename = 'Argodoc' 110 | 111 | 112 | # -- Options for LaTeX output ------------------------------------------------ 113 | 114 | latex_elements = { 115 | # The paper size ('letterpaper' or 'a4paper'). 116 | # 117 | # 'papersize': 'letterpaper', 118 | 119 | # The font size ('10pt', '11pt' or '12pt'). 120 | # 121 | # 'pointsize': '10pt', 122 | 123 | # Additional stuff for the LaTeX preamble. 124 | # 125 | # 'preamble': '', 126 | 127 | # Latex figure (float) alignment 128 | # 129 | # 'figure_align': 'htbp', 130 | } 131 | 132 | # Grouping the document tree into LaTeX files. List of tuples 133 | # (source start file, target name, title, 134 | # author, documentclass [howto, manual, or own class]). 135 | latex_documents = [ 136 | (master_doc, 'Argo.tex', 'Argo Documentation', 137 | 'Galois, Inc.', 'manual'), 138 | ] 139 | 140 | 141 | # -- Options for manual page output ------------------------------------------ 142 | 143 | # One entry per manual page. List of tuples 144 | # (source start file, name, description, authors, manual section). 145 | man_pages = [ 146 | (master_doc, 'argo', 'Argo Documentation', 147 | [author], 1) 148 | ] 149 | 150 | 151 | # -- Options for Texinfo output ---------------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | (master_doc, 'Argo', 'Argo Documentation', 158 | author, 'Argo', 'One line description of project.', 159 | 'Miscellaneous'), 160 | ] 161 | 162 | 163 | # -- Options for Epub output ------------------------------------------------- 164 | 165 | # Bibliographic Dublin Core info. 166 | epub_title = project 167 | 168 | # The unique identifier of the text. This can be a ISBN number 169 | # or the project homepage. 170 | # 171 | # epub_identifier = '' 172 | 173 | # A unique identification for the text. 174 | # 175 | # epub_uid = '' 176 | 177 | # A list of files that should not be packed into the epub file. 178 | epub_exclude_files = ['search.html'] 179 | 180 | 181 | # -- Extension configuration ------------------------------------------------- 182 | -------------------------------------------------------------------------------- /file-echo-api/README.rst: -------------------------------------------------------------------------------- 1 | file-echo-api 2 | ============= 3 | 4 | Fundamental Protocol 5 | -------------------- 6 | 7 | This application is a `JSON-RPC `_ server. Additionally, it maintains a persistent cache of application states and explicitly indicates the state in which each command is to be carried out. 8 | 9 | Transport 10 | ~~~~~~~~~ 11 | 12 | The server supports three transport methods: 13 | 14 | 15 | ``stdio`` 16 | in which the server communicates over ``stdin`` and ``stdout`` 17 | 18 | 19 | 20 | Socket 21 | in which the server communicates over ``stdin`` and ``stdout`` 22 | 23 | 24 | 25 | HTTP 26 | in which the server communicates over HTTP 27 | 28 | 29 | In both ``stdio`` and socket mode, messages are delimited using `netstrings. `_ 30 | 31 | 32 | Application State 33 | ~~~~~~~~~~~~~~~~~ 34 | 35 | According to the JSON-RPC specification, the ``params`` field in a message object must be an array or object. In this protocol, it is always an object. While each message may specify its own arguments, every message has a parameter field named ``state``. 36 | 37 | When the first message is sent from the client to the server, the ``state`` parameter should be initialized to the JSON null value ``null``. Replies from the server may contain a new state that should be used in subsequent requests, so that state changes executed by the request are visible. Prior versions of this protocol represented the initial state as the empty array ``[]``, but this is now deprecated and will be removed. 38 | 39 | In particular, per JSON-RPC, non-error replies are always a JSON object that contains a ``result`` field. The result field always contains an ``answer`` field and a ``state`` field, as well as ``stdout`` and ``stderr``. 40 | 41 | 42 | ``answer`` 43 | The value returned as a response to the request (the precise contents depend on which request was sent) 44 | 45 | 46 | 47 | ``state`` 48 | The state, to be sent in subsequent requests. If the server did not modify its state in response to the command, then this state may be the same as the one sent by the client. 49 | 50 | 51 | 52 | ``stdout`` and ``stderr`` 53 | These fields contain the contents of the Unix ``stdout`` and ``stderr`` file descriptors. They are intended as a stopgap measure for clients who are still in the process of obtaining structured information from the libraries on which they depend, so that information is not completely lost to users. However, the server may or may not cache this information and resend it. Applications are encouraged to used structured data and send it deliberately as the answer. 54 | 55 | 56 | The precise structure of states is considered an implementation detail that could change at any time. Please treat them as opaque tokens that may be saved and re-used within a given server process, but not created by the client directly. 57 | 58 | 59 | 60 | A sample server that demonstrates filesystem caching. 61 | 62 | Datatypes 63 | --------- 64 | 65 | .. _Ignorable: 66 | Ignorable data 67 | ~~~~~~~~~~~~~~ 68 | 69 | Data to be ignored can take one of three forms: 70 | 71 | 72 | ``true`` 73 | The first ignorable value 74 | 75 | 76 | 77 | ``false`` 78 | The second ignorable value 79 | 80 | 81 | 82 | ``null`` 83 | The ultimate ignorable value, neither true nor false 84 | 85 | 86 | Nothing else may be ignored. 87 | 88 | 89 | 90 | Methods 91 | ------- 92 | 93 | load (command) 94 | ~~~~~~~~~~~~~~ 95 | 96 | Load a file from disk into memory. 97 | 98 | Parameter fields 99 | ++++++++++++++++ 100 | 101 | 102 | ``file path`` 103 | The file to read into memory. 104 | 105 | 106 | 107 | Return fields 108 | +++++++++++++ 109 | 110 | No return fields 111 | 112 | 113 | 114 | clear (command) 115 | ~~~~~~~~~~~~~~~ 116 | 117 | Forget the loaded file. 118 | 119 | Parameter fields 120 | ++++++++++++++++ 121 | 122 | No parameters 123 | 124 | 125 | Return fields 126 | +++++++++++++ 127 | 128 | No return fields 129 | 130 | 131 | 132 | prepend (command) 133 | ~~~~~~~~~~~~~~~~~ 134 | 135 | Append a string to the left of the current contents. 136 | 137 | Parameter fields 138 | ++++++++++++++++ 139 | 140 | 141 | ``content`` 142 | The string to append to the left of the current file content on the server. 143 | 144 | 145 | 146 | Return fields 147 | +++++++++++++ 148 | 149 | No return fields 150 | 151 | 152 | 153 | drop (command) 154 | ~~~~~~~~~~~~~~ 155 | 156 | Drop from the left of the current contents. 157 | 158 | Parameter fields 159 | ++++++++++++++++ 160 | 161 | 162 | ``count`` 163 | The number of characters to drop from the left of the current file content on the server. 164 | 165 | 166 | 167 | Return fields 168 | +++++++++++++ 169 | 170 | No return fields 171 | 172 | 173 | 174 | implode (query) 175 | ~~~~~~~~~~~~~~~ 176 | 177 | Throw an error immediately. 178 | 179 | Parameter fields 180 | ++++++++++++++++ 181 | 182 | No parameters 183 | 184 | 185 | Return fields 186 | +++++++++++++ 187 | 188 | No return fields 189 | 190 | 191 | 192 | show (query) 193 | ~~~~~~~~~~~~ 194 | 195 | Show a substring of the file. 196 | 197 | Parameter fields 198 | ++++++++++++++++ 199 | 200 | 201 | ``start`` 202 | Start index (inclusive). If not provided, the substring is from the beginning of the file. 203 | 204 | 205 | 206 | ``end`` 207 | End index (exclusive). If not provided, the remainder of the file is returned. 208 | 209 | 210 | 211 | Return fields 212 | +++++++++++++ 213 | 214 | 215 | ``value`` 216 | The substring ranging from ``start`` to ``end``. 217 | 218 | 219 | 220 | 221 | ignore (query) 222 | ~~~~~~~~~~~~~~ 223 | 224 | Ignore an :ref:`ignorable value `. 225 | 226 | Parameter fields 227 | ++++++++++++++++ 228 | 229 | 230 | ``to be ignored`` 231 | The value to be ignored goes here. 232 | 233 | 234 | 235 | Return fields 236 | +++++++++++++ 237 | 238 | No return fields 239 | 240 | 241 | 242 | destroy state (notification) 243 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 244 | 245 | Destroy a state in the server. 246 | 247 | Parameter fields 248 | ++++++++++++++++ 249 | 250 | 251 | ``state to destroy`` 252 | The state to destroy in the server (so it can be released from memory). 253 | 254 | 255 | 256 | Return fields 257 | +++++++++++++ 258 | 259 | No return fields 260 | 261 | 262 | 263 | destroy all states (notification) 264 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 265 | 266 | Destroy all states in the server. 267 | 268 | Parameter fields 269 | ++++++++++++++++ 270 | 271 | No parameters 272 | 273 | 274 | Return fields 275 | +++++++++++++ 276 | 277 | No return fields 278 | 279 | 280 | 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /java/src/main/java/com/galois/cryptol/client/connection/JsonConnection.java: -------------------------------------------------------------------------------- 1 | package com.galois.cryptol.client.connection; 2 | 3 | import java.util.*; 4 | import java.io.*; 5 | import java.net.*; 6 | import java.util.concurrent.*; 7 | import java.util.concurrent.atomic.*; 8 | import java.util.function.*; 9 | 10 | import com.eclipsesource.json.*; 11 | 12 | import com.galois.cryptol.client.connection.*; 13 | import com.galois.cryptol.client.connection.queue.*; 14 | 15 | public class JsonConnection implements AutoCloseable { 16 | 17 | private static final String version = "2.0"; 18 | 19 | private final ConcurrentMultiQueue responseQueue = 20 | new ConcurrentMultiQueue<>(); 21 | private final AtomicInteger nextId = 22 | new AtomicInteger(0); 23 | private final Map> pendingCalls = 24 | new ConcurrentHashMap<>(); 25 | 26 | private final Pipe pipe; 27 | 28 | private volatile boolean closed = false; 29 | 30 | private static class JsonResponse { 31 | private final JsonValue result; 32 | private final JsonRpcException error; 33 | 34 | JsonResponse(JsonValue result) { 35 | this.result = result; 36 | this.error = null; 37 | } 38 | 39 | JsonResponse(JsonRpcException error) { 40 | this.result = null; 41 | this.error = error; 42 | } 43 | 44 | public JsonValue result() throws JsonRpcException { 45 | if (this.result != null) { 46 | return this.result; 47 | } else { 48 | throw this.error; 49 | } 50 | } 51 | 52 | public static JsonResponse parse(JsonObject object) { 53 | try { 54 | JsonValue result = object.get("result"); // might be null 55 | JsonValue error = object.get("error"); // might be null 56 | if (result != null && error != null) { 57 | var msg = "Both response and error fields are present"; 58 | throw new InvalidRpcResponseException(msg + ": " + object); 59 | } else if (error != null) { 60 | return new JsonResponse(new JsonRpcException(error.asObject())); 61 | } else if (result != null) { 62 | return new JsonResponse(result); 63 | } else { 64 | var msg = "Neither response nor error fields are present"; 65 | throw new InvalidRpcResponseException(msg + ": " + object); 66 | } 67 | } catch (UnsupportedOperationException e) { 68 | var msg = "Error field is not an object"; 69 | throw new InvalidRpcResponseException(msg + ": " + object, e); 70 | } 71 | } 72 | } 73 | 74 | private void sendCall(JsonValue id, JsonRpcCall call) { 75 | JsonValue message = Json.object() 76 | .add("jsonrpc", version) 77 | .add("id", id) 78 | .add("method", call.method()) 79 | .add("params", call.params()); 80 | try { 81 | pipe.send(message); 82 | } catch (Exception e) { 83 | throw new ConnectionException(e); 84 | } 85 | } 86 | 87 | private void sendNotification(JsonRpcNotification notification) { 88 | JsonValue message = Json.object() 89 | .add("jsonrpc", version) 90 | .add("method", notification.method()) 91 | .add("params", notification.params()); 92 | try { 93 | pipe.send(message); 94 | } catch (Exception e) { 95 | throw new ConnectionException(e); 96 | } 97 | } 98 | 99 | public JsonConnection(ConnectionManager connectionManager, 100 | Consumer handleException) { 101 | 102 | // When the connection dies, resend all pending calls to the new 103 | // connection -- that way, waiting calling threads don't block forever 104 | this.pipe = new ManagedPipe<>(connectionManager, () -> { 105 | pendingCalls.forEach(this::sendCall); 106 | }); 107 | 108 | Thread checkResponses = new Thread(() -> { 109 | try { 110 | while (!pipe.isClosed()) { 111 | JsonObject object; 112 | try { 113 | object = pipe.receive().asObject(); 114 | } catch (UnsupportedOperationException e) { 115 | var msg = "Response is not an object"; 116 | var err = new InvalidRpcResponseException(msg, e); 117 | throw err; 118 | } catch (ConnectionException e) { 119 | break; // The pipe cannot be re-created and is broken 120 | } catch (NoSuchElementException e) { 121 | break; // The pipe was permanently closed 122 | } 123 | JsonValue id = object.get("id"); 124 | JsonResponse response = JsonResponse.parse(object); 125 | if (id != null) { 126 | pendingCalls.remove(id); // call is no longer pending 127 | responseQueue.send(id, response); 128 | } else { 129 | try { 130 | JsonValue result = response.result(); 131 | var msg = "Non-error response had no id: " + object; 132 | var err = new InvalidRpcResponseException(msg); 133 | throw err; 134 | } catch (JsonRpcException err) { 135 | throw new UnhandledRpcException(err); 136 | } 137 | } 138 | } 139 | } catch (QueueClosedException e) { 140 | // This means responseQueue.send(id, response) discovered that 141 | // responseQueue was closed, which means someone called close() 142 | // on the connection 143 | } finally { 144 | // After exhausting the requests, close the multiqueue 145 | responseQueue.close(); 146 | } 147 | }); 148 | 149 | checkResponses.setUncaughtExceptionHandler((t, e) -> { 150 | handleException.accept(e); 151 | }); 152 | 153 | // Start the background thread 154 | checkResponses.start(); 155 | } 156 | 157 | public O call(JsonRpcCall call) throws E { 158 | JsonValue id = Json.value(nextId.getAndIncrement()); 159 | pendingCalls.put(id, call); // call is now pending 160 | this.sendCall(id, call); 161 | try { 162 | JsonValue response = responseQueue.request(id).result(); 163 | O result = call.decode(response); 164 | if (result == null) { 165 | throw new InvalidRpcCallResultException(response); 166 | } else { 167 | return result; 168 | } 169 | } catch (JsonRpcException e) { 170 | E exception = call.handle(e); 171 | if (exception == null) { 172 | throw new UnhandledRpcException(e); 173 | } else { 174 | throw exception; 175 | } 176 | } catch (QueueClosedException e) { 177 | throw new ConnectionException("Connection closed"); 178 | } 179 | } 180 | 181 | public void notify(JsonRpcNotification notification) { 182 | sendNotification(notification); 183 | } 184 | 185 | public synchronized void close() throws IOException { 186 | if (!this.closed) { 187 | this.responseQueue.close(); 188 | this.pipe.close(); 189 | this.closed = true; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /argo/src/Argo/ServerState.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | 4 | -- | The server state manages the various application states that are 5 | -- available to clients. 6 | module Argo.ServerState ( 7 | -- * The server's state, which wraps app states 8 | ServerState, 9 | initServerState, 10 | nextAppState, 11 | getAppState, 12 | destroyAppState, 13 | destroyLeastRecentState, 14 | destroyAllAppStates, 15 | statePoolCount, 16 | -- * Server launch options 17 | StateMutability(..), 18 | -- * Identifiers for states 19 | StateID, initialStateID, 20 | ) where 21 | 22 | import Control.Concurrent 23 | import Control.Monad (when) 24 | import Numeric.Natural ( Natural ) 25 | import Data.ByteString (ByteString) 26 | import qualified Data.ByteString as B 27 | import Data.Hashable (Hashable(..)) 28 | import Data.HashMap.Strict (HashMap) 29 | import qualified Data.HashMap.Strict as HM 30 | import qualified Data.Aeson as JSON 31 | import Data.IORef 32 | import Data.Set (Set) 33 | import qualified Data.Set as Set 34 | import Data.UUID (UUID) 35 | import qualified Data.UUID as UUID 36 | import qualified Data.UUID.V4 as UUID 37 | 38 | -- | A representation of protocol states to be exchanged with clients 39 | data StateID = InitialStateID | StateID UUID 40 | deriving (Eq, Ord, Show) 41 | 42 | instance Hashable StateID where 43 | hashWithSalt salt InitialStateID = 44 | salt `hashWithSalt` 45 | (0 :: Int) `hashWithSalt` 46 | () 47 | hashWithSalt salt (StateID uuid) = 48 | salt `hashWithSalt` 49 | (1 :: Int) `hashWithSalt` 50 | uuid 51 | 52 | -- | The state ID at the beginning of the interaction, represented as "null" in JSON 53 | initialStateID :: StateID 54 | initialStateID = InitialStateID 55 | 56 | instance JSON.ToJSON StateID where 57 | toJSON (StateID uuid) = JSON.String (UUID.toText uuid) 58 | toJSON InitialStateID = JSON.Null 59 | 60 | instance JSON.FromJSON StateID where 61 | parseJSON JSON.Null = pure InitialStateID 62 | -- Note: this is a backwards compatibility hack that should be removed soon 63 | parseJSON (JSON.Array arr) | arr == mempty = pure InitialStateID 64 | parseJSON other = subsequentState other 65 | where 66 | subsequentState = 67 | JSON.withText "state" $ 68 | \txt -> 69 | case UUID.fromText txt of 70 | Nothing -> mempty 71 | Just uuid -> pure (StateID uuid) 72 | 73 | 74 | 75 | 76 | -- | Describes whether the application state is pure or mutable. 77 | data StateMutability 78 | = PureState 79 | | MutableState 80 | 81 | -- | How the initial app state is stored, based on the state's mutability. 82 | data InitialAppState appState 83 | = PureInitState appState 84 | | MutableInitState (() -> IO appState) 85 | 86 | -- | Index for ordering states, e.g., to know which is oldest. 87 | newtype Index = Index {indexNat :: Natural} 88 | deriving (Eq, Show, Ord) 89 | 90 | -- | @ServerState@s contain cached application states and manage state 91 | -- identifiers. They are intended to be guarded by an 'MVar' to 92 | -- prevent the 'IORef's from being mutated inconsistently. 93 | data ServerState appState = 94 | ServerState 95 | { serverInitAppState :: !(InitialAppState appState) 96 | -- ^ State entered when server is started. 97 | , serverStatePool :: !(IORef (HashMap UUID (Index, appState))) 98 | -- ^ Currently active states and their order int. 99 | , serverStateOrdering :: !(IORef (Set (Index, UUID))) 100 | -- ^ Set used to provide constant-time oldest state calculation 101 | -- (i.e., since pair ordering is calculated pointwise left-to-right). 102 | , serverIndex :: !(IORef Index) 103 | } 104 | 105 | -- | Get the next index and increment the server's index. 106 | nextIndex :: ServerState appState -> IO Index 107 | nextIndex server = do 108 | idx <- readIORef $ serverIndex server 109 | writeIORef (serverIndex server) (Index $ (indexNat idx) + 1) 110 | pure idx 111 | 112 | -- | Construct an initial server state, given a means of constructing 113 | -- the initial application state and a bound on the number of states 114 | -- to keep cached. 115 | initServerState :: 116 | StateMutability -> 117 | ((FilePath -> IO ByteString) -> IO appState) -> 118 | IO (ServerState appState) 119 | initServerState mut mkInitState = do 120 | initState <- case mut of 121 | PureState -> PureInitState <$> (mkInitState B.readFile) 122 | MutableState -> pure $ MutableInitState $ \_ -> mkInitState $ B.readFile 123 | emptyStatePool <- newIORef HM.empty 124 | emptyStateOrdering <- newIORef Set.empty 125 | idx <- newIORef (Index 0) 126 | pure $ 127 | ServerState 128 | { serverInitAppState = initState 129 | , serverStatePool = emptyStatePool 130 | , serverStateOrdering = emptyStateOrdering 131 | , serverIndex = idx 132 | } 133 | 134 | statePoolCount :: ServerState appState -> IO Natural 135 | statePoolCount server = 136 | fromInteger . toInteger . HM.size <$> readIORef (serverStatePool server) 137 | 138 | -- | Given a fresh application state and the StateID it was derived from, return 139 | -- a fresh state ID that uniquely describes the new state and destroy the 140 | -- previous state (if possible). This ASSUMES the server state MVar is 141 | -- already locked elsewhere thus preventing inconsistent server states. 142 | nextAppState :: 143 | ServerState appState -> 144 | StateID {-^ State ID from which the new state originated (i.e., its parent) -} -> 145 | appState {-^ The new application state -} -> 146 | IO StateID 147 | nextAppState server prevStateID newAppState = do 148 | destroyAppState' server prevStateID 149 | uuid <- UUID.nextRandom 150 | idx <- nextIndex server 151 | modifyIORef' (serverStatePool server) $ HM.insert uuid (idx, newAppState) 152 | modifyIORef' (serverStateOrdering server) $ Set.insert (idx, uuid) 153 | return $ StateID uuid 154 | 155 | -- | Like @destroyAppState@ but ASSUMES the concerns regarding server state 156 | -- consistency have already been handled (i.e., someone has already acquired the 157 | -- MVar for the server state). 158 | destroyAppState' :: 159 | ServerState s -> 160 | StateID -> 161 | IO () 162 | destroyAppState' _server InitialStateID = pure () 163 | destroyAppState' server (StateID uuid) = do 164 | HM.lookup uuid <$> readIORef (serverStatePool server) >>= \case 165 | Nothing -> pure () 166 | Just (idx, _) -> modifyIORef' (serverStateOrdering server) $ Set.delete (idx, uuid) 167 | modifyIORef' (serverStatePool server) $ HM.delete uuid 168 | 169 | -- | Destroy a non-initial app state so it is no longer available for requests. 170 | -- Explicitly requires the @MVar (ServerState s)@ so a notification---an action 171 | -- which does not inherently require a lock on the server's state per se---can 172 | -- delete a state while being sure some process isn't actively operating on it 173 | -- as well. 174 | destroyAppState :: 175 | MVar (ServerState s) -> 176 | StateID -> 177 | IO () 178 | destroyAppState serverMVar sid = 179 | withMVar serverMVar $ \server -> destroyAppState' server sid 180 | 181 | 182 | -- | Destroys the oldest state currently in cache. N.B., this function 183 | -- assumes the caller is maintaining invariants related to MVars 184 | -- and state shared between threads. 185 | destroyLeastRecentState :: 186 | ServerState s -> 187 | IO () 188 | destroyLeastRecentState server = do 189 | orderSet <- readIORef (serverStateOrdering server) 190 | when (Set.size orderSet > 0) $ do 191 | let (_, uuid) = Set.elemAt 0 orderSet 192 | destroyAppState' server (StateID uuid) 193 | 194 | -- | Like @destroyAppState@ but destroys all non-initial app states. 195 | destroyAllAppStates :: 196 | MVar (ServerState s) -> 197 | IO () 198 | destroyAllAppStates serverMVar = 199 | withMVar serverMVar $ \server -> 200 | writeIORef (serverStatePool server) $ HM.empty 201 | 202 | -- | Retrieve the application state that corresponds to a given state ID. 203 | -- 204 | -- If given the initial state ID and the server state is mutable, 205 | -- a new initial state will be returned. 206 | -- 207 | -- If the state ID is not known, returns Nothing. 208 | getAppState :: 209 | ServerState appState -> 210 | StateID -> 211 | IO (Maybe appState) 212 | getAppState server InitialStateID = 213 | case (serverInitAppState server) of 214 | PureInitState s -> pure $ Just s 215 | MutableInitState f -> Just <$> f () 216 | getAppState server (StateID uuid) = do 217 | pool <- readIORef (serverStatePool server) 218 | pure $ snd <$> (HM.lookup uuid pool) 219 | -------------------------------------------------------------------------------- /file-echo-api/src/MutableFileEchoServer.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE MultiParamTypeClasses #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {- Like FileEchoServer but the underlying state uses mutability 4 | to update the loaded file contents. -} 5 | module MutableFileEchoServer ( module MutableFileEchoServer ) where 6 | 7 | import qualified Argo as Argo 8 | import qualified Argo.Doc as Doc 9 | import Control.Concurrent ( threadDelay ) 10 | import Control.Monad.IO.Class ( liftIO ) 11 | import qualified Data.Aeson as JSON 12 | import Data.Aeson ( (.:), (.:?), (.=), (.!=) ) 13 | import Data.ByteString ( ByteString ) 14 | import Data.IORef ( IORef, newIORef, readIORef, writeIORef) 15 | import qualified Data.ByteString.Char8 as Char8 16 | import qualified Data.Text as T 17 | import Data.Time.Clock.POSIX 18 | import Data.Scientific 19 | import qualified System.Directory as Dir 20 | 21 | 22 | 23 | newtype FileContents = FileContents String 24 | 25 | data ServerState = ServerState 26 | { loadedFile :: Maybe FilePath 27 | -- ^ Loaded file (if any). 28 | , fileContents :: IORef FileContents 29 | -- ^ Current file contents, or "" if one has not been loaded yet. 30 | } 31 | 32 | initialState :: 33 | Maybe FilePath -> 34 | (FilePath -> IO ByteString) -> 35 | IO ServerState 36 | initialState Nothing _reader = do 37 | contentRef <- newIORef (FileContents "") 38 | pure $ ServerState Nothing contentRef 39 | initialState (Just path) reader = do 40 | contents <- FileContents . Char8.unpack <$> reader path 41 | contentRef <- newIORef contents 42 | pure $ ServerState (Just path) contentRef 43 | 44 | newtype ServerErr = ServerErr String 45 | newtype ServerRes a = ServerRes (Either ServerErr (a, FileContents)) 46 | newtype ServerCmd a = 47 | ServerCmd ((FilePath -> IO ByteString, FileContents) -> IO (ServerRes a)) 48 | 49 | 50 | ------------------------------------------------------------------------ 51 | -- Errors 52 | 53 | fileNotFound :: FilePath -> Argo.JSONRPCException 54 | fileNotFound fp = 55 | Argo.makeJSONRPCException 56 | 20051 (T.pack ("File doesn't exist: " <> fp)) 57 | (Just (JSON.object ["path" .= fp])) 58 | 59 | ------------------------------------------------------------------------ 60 | -- Load Command 61 | 62 | data LoadParams = LoadParams FilePath 63 | 64 | instance JSON.FromJSON LoadParams where 65 | parseJSON = 66 | JSON.withObject "params for \"load\"" $ 67 | \o -> LoadParams <$> o .: "file path" 68 | 69 | instance Doc.DescribedMethod LoadParams () where 70 | parameterFieldDescription = 71 | [("file path", 72 | Doc.Paragraph [Doc.Text "The file to read into memory."])] 73 | 74 | loadCmd :: LoadParams -> Argo.Command ServerState () 75 | loadCmd (LoadParams file) = 76 | do exists <- liftIO $ Dir.doesFileExist file 77 | if exists 78 | then do getFileContents <- Argo.getFileReader 79 | contents <- liftIO $ getFileContents file 80 | appState <- Argo.getState 81 | liftIO $ writeIORef (fileContents appState) $ FileContents $ Char8.unpack contents 82 | Argo.setState $ appState { loadedFile = Just file } 83 | else Argo.raise (fileNotFound file) 84 | 85 | 86 | ------------------------------------------------------------------------ 87 | -- Clear Command 88 | 89 | data ClearParams = ClearParams 90 | 91 | instance JSON.FromJSON ClearParams where 92 | parseJSON = 93 | JSON.withObject "params for \"show\"" $ 94 | \_ -> pure ClearParams 95 | 96 | instance Doc.DescribedMethod ClearParams () where 97 | parameterFieldDescription = [] 98 | 99 | clearCmd :: ClearParams -> Argo.Command ServerState () 100 | clearCmd _ = do 101 | appState <- Argo.getState 102 | liftIO $ writeIORef (fileContents appState) $ FileContents "" 103 | Argo.setState $ appState { loadedFile = Nothing } 104 | 105 | ------------------------------------------------------------------------ 106 | -- Show Command 107 | 108 | data ShowParams = ShowParams 109 | { showStart :: Int 110 | -- ^ Inclusive start index in contents. 111 | , showEnd :: Maybe Int 112 | -- ^ Exclusive end index in contents. 113 | } 114 | 115 | instance JSON.FromJSON ShowParams where 116 | parseJSON = 117 | JSON.withObject "params for \"show\"" $ 118 | \o -> do start <- o .:? "start" .!= 0 119 | end <- o .:? "end" 120 | pure $ ShowParams start end 121 | 122 | instance Doc.DescribedMethod ShowParams JSON.Value where 123 | parameterFieldDescription = 124 | [ ("start", 125 | Doc.Paragraph [Doc.Text "Start index (inclusive). If not provided, the substring is from the beginning of the file."]) 126 | , ("end", Doc.Paragraph [Doc.Text "End index (exclusive). If not provided, the remainder of the file is returned."]) 127 | ] 128 | 129 | resultFieldDescription = 130 | [ ("value", 131 | Doc.Paragraph [ Doc.Text "The substring ranging from " 132 | , Doc.Literal "start", Doc.Text " to ", Doc.Literal "end" 133 | , Doc.Text "." ]) 134 | ] 135 | 136 | 137 | showCmd :: ShowParams -> Argo.Query ServerState JSON.Value 138 | showCmd (ShowParams start end) = do 139 | appState <- Argo.getState 140 | (FileContents contents) <- liftIO $ readIORef $ fileContents appState 141 | let len = case end of 142 | Nothing -> length contents 143 | Just idx -> idx - start 144 | pure (JSON.object [ "value" .= JSON.String (T.pack $ take len $ drop start contents)]) 145 | 146 | ------------------------------------------------------------------------ 147 | -- Destroy State Command 148 | data DestroyStateParams = 149 | DestroyStateParams 150 | { 151 | stateToDestroy :: !Argo.StateID 152 | } 153 | 154 | instance JSON.FromJSON DestroyStateParams where 155 | parseJSON = 156 | JSON.withObject "params for \"destroy state\"" $ 157 | \o -> DestroyStateParams <$> o .: "state to destroy" 158 | 159 | instance Doc.DescribedMethod DestroyStateParams () where 160 | parameterFieldDescription = 161 | [("state to destroy", 162 | Doc.Paragraph [Doc.Text "The state to destroy in the server (so it can be released from memory)."]) 163 | ] 164 | 165 | destroyState :: DestroyStateParams -> Argo.Notification () 166 | destroyState (DestroyStateParams stateID) = Argo.destroyState stateID 167 | 168 | 169 | ------------------------------------------------------------------------ 170 | -- Sleep Query 171 | 172 | newtype SleepParams = SleepParams Int 173 | 174 | instance JSON.FromJSON SleepParams where 175 | parseJSON = 176 | JSON.withObject "params for \"sleep\"" $ 177 | \o -> SleepParams <$> o .: "microseconds" 178 | 179 | instance Doc.DescribedMethod SleepParams JSON.Value where 180 | parameterFieldDescription = 181 | [("microseconds", 182 | Doc.Paragraph [Doc.Text "The duration to sleep in microseconds."])] 183 | 184 | resultFieldDescription = 185 | [ ("value", 186 | Doc.Paragraph [ Doc.Text "Duration in seconds sleep lasted."]) 187 | ] 188 | 189 | sleepQuery :: SleepParams -> Argo.Query ServerState JSON.Value 190 | sleepQuery (SleepParams ms) = liftIO $ do 191 | t1 <- round `fmap` getPOSIXTime 192 | threadDelay ms 193 | t2 <- round `fmap` getPOSIXTime 194 | pure (JSON.object [ "value" .= (JSON.Number (scientific (t2 - t1) 0))]) 195 | 196 | 197 | 198 | ------------------------------------------------------------------------ 199 | -- Interrupt All Threads Command 200 | data InterruptAllThreadsParams = InterruptAllThreadsParams 201 | 202 | instance JSON.FromJSON InterruptAllThreadsParams where 203 | parseJSON = 204 | JSON.withObject "params for \"interrupt all threads\"" $ 205 | \_ -> pure InterruptAllThreadsParams 206 | 207 | instance Doc.DescribedMethod InterruptAllThreadsParams () where 208 | parameterFieldDescription = [] 209 | 210 | 211 | interruptAllThreads :: InterruptAllThreadsParams -> Argo.Notification () 212 | interruptAllThreads _ = Argo.interruptAllThreads 213 | 214 | 215 | ------------------------------------------------------------------------ 216 | -- SlowClear Command 217 | 218 | newtype SlowClear = SlowClear Int 219 | 220 | instance JSON.FromJSON SlowClear where 221 | parseJSON = 222 | JSON.withObject "params for \"slow clear\"" $ 223 | \o -> SlowClear <$> o .: "pause microseconds" 224 | 225 | instance Doc.DescribedMethod SlowClear () where 226 | parameterFieldDescription = 227 | [("pause microseconds", 228 | Doc.Paragraph [Doc.Text "The duration to sleep in microseconds between each character being cleared."])] 229 | 230 | slowClear :: SlowClear -> Argo.Command ServerState () 231 | slowClear (SlowClear ms) = do 232 | appState <- Argo.getState 233 | let go = do (FileContents contents) <- liftIO $ readIORef (fileContents appState) 234 | case contents of 235 | [] -> Argo.modifyState $ \s -> s { loadedFile = Nothing } 236 | (_:cs) -> do 237 | liftIO $ writeIORef (fileContents appState) $ FileContents cs 238 | liftIO $ threadDelay ms 239 | go 240 | go 241 | -------------------------------------------------------------------------------- /tasty-script-exitcode/src/Test/Tasty/HUnit/ScriptExit.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE NamedFieldPuns #-} 3 | 4 | module Test.Tasty.HUnit.ScriptExit where 5 | 6 | import Test.Tasty 7 | import Test.Tasty.HUnit 8 | import System.Directory 9 | import System.Exit 10 | import System.FilePath 11 | import System.IO.Temp (withSystemTempDirectory) 12 | import System.Info (os) 13 | import System.Process 14 | import Control.Applicative 15 | import Data.List 16 | import Data.Map (Map) 17 | import qualified Data.Map as Map 18 | 19 | -- | Given a directory path and a list of @TestLang@s to use, create a list of 20 | -- named tests (one per @TestLang@), each of which when run will test all of the 21 | -- script files for that particular language found in the given directory path. 22 | -- A test is considered to have passed if it exits with code 0; otherwise, it 23 | -- will fail and print the contents of its stdout and stderr as part of the test 24 | -- failure message. 25 | -- 26 | -- A note on duplicate @TestLang@ entries: if multiple @TestLang@s have the same 27 | -- extension, only one of them will be used; if multiple @TestLang@s have the 28 | -- same language name, they will be grouped together even if their extensions 29 | -- differ (this may be desirable in the case that the scripting language can 30 | -- interpret multiple different kinds of files but may need different 31 | -- command-line setup to do so). 32 | makeScriptTests :: FilePath -> [TestLang] -> IO [TestTree] 33 | makeScriptTests scriptTestDir testLanguages = 34 | do scriptTestFiles <- map (scriptTestDir ) <$> listDirectory scriptTestDir 35 | pure $ perLanguageTests testLanguages scriptTestFiles 36 | 37 | -- | Defines the information necessary to run an exit-code test for another 38 | -- language (usually a scripting language like Python). 39 | data TestLang 40 | = TestLang 41 | { testLangName :: String -- ^ The language name (for test output) 42 | , testLangExtension :: String -- ^ The extension for its scripts 43 | , testLangExecutable :: String -- ^ The executable to invoke 44 | , testLangArgsFormat :: FilePath -> [String] 45 | -- ^ Given a path to a script, how to list the arguments to the 46 | -- scripting language executable (usually this is just @\x -> [x]@ but 47 | -- not always) 48 | } 49 | 50 | -- | The Python 3 scripting language: this definition assumes an executable 51 | -- called @python3@ lives on your @$PATH@. 52 | python3 :: TestLang 53 | python3 = 54 | TestLang 55 | { testLangName = "Python 3" 56 | , testLangExtension = ".py" 57 | , testLangExecutable = "python3" 58 | , testLangArgsFormat = \file -> [file] 59 | } 60 | 61 | mypy :: TestLang 62 | mypy = 63 | TestLang 64 | { testLangName = "MyPy (Python 3)" 65 | , testLangExtension = ".py" 66 | , testLangExecutable = "mypy" 67 | , testLangArgsFormat = \file -> [file] 68 | } 69 | 70 | -- | Python 3 in a virtual environment: this definition assumes that 71 | -- an executable named @python3@ is present in @$PATH@. The first 72 | -- argument is a description of the Python packages to make available 73 | -- in the virtual environment, expressed in the form of a path to the 74 | -- standard @requirements.txt@ file that lists the packages. The 75 | -- second argument consists of the tests to be run in this 76 | -- environment, which will be provided with a means of invoking @pip@ 77 | -- and a scripting language. The means of invoking @pip@ is a function 78 | -- that, when provided with an argument list for @pip@, returns an 79 | -- @IO@ action that provides the virtual environment's @pip@ with 80 | -- those arguments. If @pip@ fails, the tests all fail. 81 | withPython3venv :: 82 | Maybe FilePath {- ^ The path to requirements.txt, if desired -} -> 83 | (([String] -> IO ()) -> TestLang -> IO a) {- ^ The tests that run using the virtual environment, given pip and Python -}-> 84 | IO a 85 | withPython3venv requirements todo = 86 | withSystemTempDirectory "virtenv" $ \venvDir -> 87 | -- Some systems install Python 3 as `python3`, but some call it `python`. 88 | do mpy3 <- findExecutable "python3" 89 | mpy <- findExecutable "python" 90 | pyExe <- case mpy3 <|> mpy of 91 | Just exeName -> return exeName 92 | Nothing -> assertFailure "Python executable not found." 93 | let process = proc pyExe ["-m", "venv", venvDir] 94 | binDir = if os == "mingw32" then "Scripts" else "bin" 95 | pipInstall = proc (venvDir binDir "python") ["-m", "pip", "install", "--upgrade", "pip"] 96 | (exitCode, stdout, stderr) <- readCreateProcessWithExitCode process "" 97 | case exitCode of 98 | ExitFailure code -> 99 | assertFailure $ 100 | "Failed to create virtualenv at \"" <> venvDir <> "\" "<> 101 | "with code " <> show code <> ": " <> 102 | ":\nstdout: " <> stdout <> "\nstderr: " <> stderr 103 | ExitSuccess -> 104 | let venvPython = 105 | TestLang 106 | { testLangName = "Python in virtualenv" 107 | , testLangExtension = ".py" 108 | , testLangExecutable = venvDir binDir "python" 109 | , testLangArgsFormat = \file -> [file] 110 | } 111 | pip args = 112 | let pipProc = proc (venvDir binDir "pip") args in 113 | readCreateProcessWithExitCode pipProc "" >>= 114 | \case 115 | (ExitFailure code, pipStdout, pipStderr) -> 116 | assertFailure $ 117 | "pip failed in environment \"" <> venvDir <> "\" "<> 118 | "with code " <> show code <> ": " <> 119 | ":\nstdout: " <> pipStdout <> "\nstderr: " <> pipStderr 120 | (ExitSuccess, _, _) -> 121 | pure () 122 | in do (exitCode, stdout, stderr) <- readCreateProcessWithExitCode pipInstall "" 123 | case exitCode of 124 | ExitFailure code -> 125 | assertFailure $ 126 | "Failed to install `pip` with code " <> 127 | show code <> ": " <> 128 | ":\nstdout: " <> stdout <> "\nstderr: " <> stderr 129 | ExitSuccess -> do 130 | traverse (\reqPath -> pip ["install", "-r", reqPath]) requirements 131 | todo pip venvPython 132 | 133 | 134 | -- | Given a list of @TestLang@s to use and a list of possible script 135 | -- filespaths, generate a list of named tests corresponding to the exit-code 136 | -- tests for each of the specified languages. This function has the same details 137 | -- and caveats as 'makeScriptTests'. 138 | perLanguageTests :: [TestLang] -> [FilePath] -> [TestTree] 139 | perLanguageTests testLanguages = 140 | toTestTrees . foldr addScript Map.empty 141 | where 142 | languageSupport :: Map String [(String, FilePath -> TestTree)] 143 | languageSupport = 144 | Map.fromListWith (++) $ 145 | map (\TestLang{testLangName, 146 | testLangExtension, 147 | testLangExecutable, 148 | testLangArgsFormat} -> 149 | (testLangExtension, 150 | [(testLangName, 151 | scriptTest testLangExecutable testLangArgsFormat)])) 152 | testLanguages 153 | 154 | addScript :: FilePath -> Map String [TestTree] -> Map String [TestTree] 155 | addScript fileName tests = 156 | case Map.lookup (takeExtension fileName) languageSupport of 157 | Nothing -> tests 158 | Just relevantLangs -> 159 | foldr 160 | (\(language, makeTest) -> 161 | Map.insertWith (++) language [makeTest fileName]) 162 | tests 163 | relevantLangs 164 | 165 | toTestTrees :: Map String [TestTree] -> [TestTree] 166 | toTestTrees = 167 | map (uncurry testGroup) . Map.assocs 168 | 169 | -- | Make an individual script test: given an executable name (either a full 170 | -- path or some executable on the @$PATH@), a way to format an input script 171 | -- filename as arguments, and an input script path, create a single unit test 172 | -- which invokes the scripting language on the script file with an empty 173 | -- @stdin@, and fails when it exits with some exit code other than 0. The result 174 | -- of such test failures includes the exit code as well as the full contents of 175 | -- the process's @stdout@ and @stderr@. 176 | scriptTest :: FilePath -> (FilePath -> [String]) -> FilePath -> TestTree 177 | scriptTest execPath makeArgs scriptPath = 178 | testCase (takeFileName scriptPath) $ 179 | do let args = makeArgs scriptPath 180 | process = proc execPath args 181 | (exitCode, stdout, stderr) <- readCreateProcessWithExitCode process "" 182 | case exitCode of 183 | ExitSuccess -> pure () 184 | ExitFailure code -> 185 | assertFailure $ 186 | "Exit code " <> show code <> ": " 187 | <> execPath <> " " <> concat (intersperse " " args) 188 | <> ":\nstdout: " <> stdout <> "\nstderr: " <> stderr 189 | 190 | 191 | -- | Given the name of the pyhon executable, 192 | -- run `python -m unittest discover` via 193 | -- readProcessWithExitCode and return the result. 194 | runPythonUnitTests :: String -> IO (ExitCode, String, String) 195 | runPythonUnitTests pyExeName = 196 | let args = ["-m", "unittest", "discover"] 197 | in readProcessWithExitCode pyExeName args "" 198 | -------------------------------------------------------------------------------- /python/tests/test_file_echo_interaction.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import argo_client.interaction as argo 4 | import argo_client.connection as argo_conn 5 | from pathlib import Path 6 | from argo_client.interaction import HasProtocolState, ArgoException 7 | from argo_client.connection import ServerConnection, StdIOProcess 8 | from typing import Any, Optional, TextIO 9 | import sys 10 | import signal 11 | import time 12 | import subprocess 13 | 14 | class LoadFile(argo.Command): 15 | def __init__(self, connection : HasProtocolState, file_path : str) -> None: 16 | super(LoadFile, self).__init__('load', {'file path': file_path}, connection, timeout=None) 17 | 18 | def process_result(self, res : Any) -> Any: 19 | return res 20 | 21 | class Implode(argo.Command): 22 | def __init__(self, connection : HasProtocolState) -> None: 23 | super(Implode, self).__init__('implode', {}, connection, timeout=None) 24 | 25 | def process_result(self, res : Any) -> Any: 26 | return res 27 | 28 | class Show(argo.Query): 29 | def __init__(self, connection : HasProtocolState, start : Optional[int], end : Optional[int]) -> None: 30 | params = {'state': connection.protocol_state()} 31 | if start is not None: 32 | params['start'] += start 33 | if end is not None: 34 | params['end'] += end 35 | super(Show, self).__init__('show', params, connection, timeout=None) 36 | 37 | def process_result(self, res : Any) -> Any: 38 | return res['value'] 39 | 40 | class Reset(argo.Notification): 41 | def __init__(self, connection : HasProtocolState) -> None: 42 | super(Reset, self).__init__('destroy state', {'state to destroy': connection.protocol_state()}, connection) 43 | 44 | 45 | class FileEchoConnection: 46 | most_recent_result : Optional[argo.Interaction] 47 | server_connection : ServerConnection 48 | 49 | def __init__(self, connection : ServerConnection): 50 | self.most_recent_result = None 51 | self.server_connection = connection 52 | 53 | def protocol_state(self) -> Any: 54 | if self.most_recent_result is None: 55 | return None 56 | else: 57 | return self.most_recent_result.state() 58 | 59 | 60 | def load_file(self, filename : str) -> argo.Command: 61 | """Load a file's contents into the server. 62 | """ 63 | self.most_recent_result = LoadFile(self, filename) 64 | return self.most_recent_result 65 | 66 | def implode(self) -> argo.Command: 67 | """Cause an internal server error. 68 | """ 69 | self.most_recent_result = Implode(self) 70 | return self.most_recent_result 71 | 72 | def show(self, *, start : int = None, end : int = None) -> argo.Query: 73 | """Load a file's contents into the server. 74 | """ 75 | self.most_recent_result = Show(self, start = start, end = end) 76 | return self.most_recent_result 77 | 78 | def reset(self) -> None: 79 | """Reset the underlying server state.""" 80 | Reset(self) 81 | self.most_recent_result = None 82 | 83 | def logging(self, on : bool, *, dest : TextIO = sys.stderr) -> None: 84 | """Whether to log received and transmitted JSON.""" 85 | self.server_connection.logging(on=on,dest=dest) 86 | 87 | dir_path = Path(os.path.dirname(os.path.realpath(__file__))) 88 | file_dir = dir_path.joinpath('test-data') 89 | if not file_dir.is_dir(): 90 | print('ERROR: ' + str(file_dir) + ' is not a directory!') 91 | assert(False) 92 | 93 | class BasicInteractionTests(unittest.TestCase): 94 | # Connection to server 95 | c : FileEchoConnection = None 96 | 97 | @classmethod 98 | def setUpClass(self): 99 | self.c = FileEchoConnection( 100 | argo.ServerConnection( 101 | StdIOProcess( 102 | "cabal run exe:file-echo-api --verbose=0 -- stdio"))) 103 | 104 | def test_basics(self): 105 | c = self.c 106 | hello_file = file_dir.joinpath('hello.txt') 107 | self.assertTrue(False if not hello_file.is_file() else True) 108 | 109 | # test loading and showing a valid file 110 | c.load_file(str(hello_file)) 111 | self.assertEqual(c.show().result(), "Hello World!\n") 112 | 113 | c.reset() 114 | 115 | 116 | class CommandErrorInteractionTests1(unittest.TestCase): 117 | # Connection to server 118 | c : FileEchoConnection = None 119 | 120 | @classmethod 121 | def setUpClass(self): 122 | self.c = FileEchoConnection( 123 | argo.ServerConnection( 124 | StdIOProcess( 125 | "cabal run exe:file-echo-api --verbose=0 -- stdio"))) 126 | 127 | def test_missing_file(self): 128 | c = self.c 129 | 130 | # test loading a non-existant file 131 | halo_file = file_dir.joinpath('halo.txt') # <- file doesn't exist 132 | self.assertFalse(False if not halo_file.is_file() else True) 133 | with self.assertRaises(ArgoException): 134 | c.load_file(str(halo_file)).result() 135 | 136 | # test that internal errors without extra data raise proper exceptions 137 | with self.assertRaises(ArgoException): 138 | c.implode().result() 139 | 140 | 141 | class CommandErrorInteractionTests2(unittest.TestCase): 142 | # Connection to server 143 | c : FileEchoConnection = None 144 | 145 | @classmethod 146 | def setUpClass(self): 147 | self.c = FileEchoConnection( 148 | argo.ServerConnection( 149 | StdIOProcess( 150 | "cabal run exe:file-echo-api --verbose=0 -- stdio"))) 151 | 152 | def test_implosion_after_load(self): 153 | c = self.c 154 | 155 | hello_file = file_dir.joinpath('hello.txt') 156 | self.assertTrue(False if not hello_file.is_file() else True) 157 | 158 | # test loading and showing a valid file 159 | c.load_file(str(hello_file)) 160 | self.assertEqual(c.show().result(), "Hello World!\n") 161 | 162 | # test that internal errors without extra data raise proper exceptions 163 | with self.assertRaises(ArgoException): 164 | c.implode().result() 165 | 166 | 167 | class CommandErrorInteractionTests3(unittest.TestCase): 168 | # Connection to server 169 | c : FileEchoConnection = None 170 | 171 | @classmethod 172 | def setUpClass(self): 173 | self.c = FileEchoConnection( 174 | argo.ServerConnection( 175 | StdIOProcess( 176 | "cabal run exe:file-echo-api --verbose=0 -- stdio"))) 177 | 178 | def test_only_implision(self): 179 | c = self.c 180 | 181 | # test that internal errors without extra data raise proper exceptions 182 | with self.assertRaises(ArgoException): 183 | c.implode().result() 184 | 185 | 186 | class CommandErrorInteractionTests4(unittest.TestCase): 187 | # Connection to server 188 | c : FileEchoConnection = None 189 | 190 | @classmethod 191 | def setUpClass(self): 192 | self.c = FileEchoConnection( 193 | argo.ServerConnection( 194 | StdIOProcess( 195 | "cabal run exe:file-echo-api --verbose=0 -- stdio"))) 196 | 197 | def test_load_after_reset(self): 198 | c = self.c 199 | 200 | hello_file = file_dir.joinpath('hello.txt') 201 | self.assertTrue(False if not hello_file.is_file() else True) 202 | 203 | # test loading and showing a valid file 204 | c.load_file(str(hello_file)) 205 | self.assertEqual(c.show().result(), "Hello World!\n") 206 | 207 | c.reset() 208 | 209 | # post reset connection is in initial state 210 | self.assertEqual(c.show().result(), "") 211 | 212 | # test loading and showing a valid file after a reset 213 | base_file = file_dir.joinpath('base.txt') 214 | self.assertTrue(False if not base_file.is_file() else True) 215 | c.load_file(str(base_file)) 216 | self.assertEqual(c.show().result(), "All your base are belong to us!\n") 217 | 218 | 219 | class CommandErrorInteractionTests5(unittest.TestCase): 220 | # Connection to server 221 | c : FileEchoConnection = None 222 | 223 | @classmethod 224 | def setUpClass(self): 225 | p = subprocess.Popen( 226 | ["cabal", "run", "exe:file-echo-api", "--verbose=0", "--", 227 | "socket", "--port", "50005" #, "--log", "stderr" 228 | ], # Uncomment the above for debug output 229 | stdout=subprocess.PIPE, 230 | stdin=subprocess.DEVNULL, 231 | #stderr=subprocess.PIPE, 232 | stderr=sys.stdout, 233 | start_new_session=True) 234 | time.sleep(3) 235 | assert(p is not None) 236 | poll_result = p.poll() 237 | if poll_result is not None: 238 | print(poll_result) 239 | print(p.stdout.read()) 240 | print(p.stderr.read()) 241 | assert(poll_result is None) 242 | self.p = p 243 | self.c = FileEchoConnection( 244 | argo_conn.ServerConnection( 245 | argo_conn.RemoteSocketProcess('localhost', 50005, ipv6=True))) 246 | 247 | @classmethod 248 | def tearDownClass(self): 249 | os.killpg(os.getpgid(self.p.pid), signal.SIGKILL) 250 | super().tearDownClass() 251 | 252 | def test_load_after_implosion(self): 253 | c = self.c 254 | self.c.logging(False) # Change this to 'True' for debug output 255 | 256 | # test that internal errors without extra data raise proper exceptions 257 | with self.assertRaises(ArgoException): 258 | c.implode().result() 259 | 260 | hello_file = file_dir.joinpath('hello.txt') 261 | self.assertTrue(False if not hello_file.is_file() else True) 262 | 263 | # test that loading and showing a valid file still works after an 264 | # exception 265 | c.load_file(str(hello_file)) 266 | self.assertEqual(c.show().result(), "Hello World!\n") 267 | 268 | # test that a reset still works after an exception 269 | c.reset() -------------------------------------------------------------------------------- /python/argo_client/interaction.py: -------------------------------------------------------------------------------- 1 | """Higher-level tracking of the semantics of specific commands.""" 2 | 3 | from argo_client.connection import ServerConnection 4 | 5 | from abc import abstractmethod 6 | from typing import Any, Dict, Optional, Tuple 7 | from typing_extensions import Protocol 8 | 9 | 10 | class HasServerConnection(Protocol): 11 | server_connection: ServerConnection 12 | 13 | class HasProtocolState(Protocol): 14 | def protocol_state(self) -> Any: ... 15 | server_connection: ServerConnection 16 | 17 | 18 | class ArgoException(Exception): 19 | message: str 20 | data: Dict[str, Any] 21 | code: int 22 | stdout: str 23 | stderr: str 24 | """A Python representation of the underlying JSON RPC error.""" 25 | 26 | def __init__(self, 27 | code: int, 28 | message: str, 29 | data: Any, 30 | stdout: str, 31 | stderr: str) -> None: 32 | super().__init__(message) 33 | self.message = message 34 | self.data = data 35 | self.code = code 36 | self.stdout = stdout 37 | self.stderr = stderr 38 | 39 | 40 | class Interaction: 41 | """A representation of a concrete stateful interaction (i.e., a command or query) 42 | with the server. Applications should subclass this according to their needs. 43 | 44 | Subclasses should call the superclass constructor with the method 45 | name and the parameters. They should additionally implement methods 46 | ``state()`` and ``result()`` that return the protocol state and 47 | the result from a non-error response to the method. Both methods 48 | may call ``self.raw_result()`` to get the JSON RPC response 49 | associated with the resquest. 50 | 51 | """ 52 | _method: str 53 | _params: Dict[str, Any] 54 | 55 | def __init__(self, method: str, params: Dict[str, Any], 56 | connection: HasProtocolState, 57 | *, 58 | timeout: Optional[float]) -> None: 59 | 60 | self.connection = connection 61 | self._raw_response = None 62 | self.init_state = connection.protocol_state() 63 | self._method = method 64 | self._params = params 65 | self.add_param('state', self.init_state) 66 | self.request_id = \ 67 | connection. \ 68 | server_connection. \ 69 | send_command(self._method, self._params, timeout=timeout) 70 | 71 | def add_param(self, name: str, val: Any) -> None: 72 | self._params[name] = val 73 | 74 | def raw_result(self) -> Any: 75 | """Get the JSON response associated with the request. Blocks until the 76 | reply is received. 77 | """ 78 | if self._raw_response is None: 79 | self._raw_response = \ 80 | self.connection. \ 81 | server_connection. \ 82 | wait_for_reply_to(self.request_id) 83 | return self._raw_response 84 | 85 | @abstractmethod 86 | def state(self) -> Any: 87 | """Subclasses should implement this method to return the protocol 88 | state after the RPC call is complete. This should be obtained from 89 | the result of ``self.raw_result()``. 90 | """ 91 | pass 92 | 93 | @abstractmethod 94 | def result(self) -> Any: 95 | """Subclasses should implement this method to return the protocol 96 | result after the RPC call is complete and succeeds. This 97 | should be obtained from the result of ``self.raw_result()``. 98 | """ 99 | pass 100 | 101 | @abstractmethod 102 | def stdout(self) -> str: 103 | """Subclasses should implement this method to report the stdout yielded 104 | by the interaction. This should be obtained from the result of 105 | ``self.raw_result()``.""" 106 | pass 107 | 108 | @abstractmethod 109 | def stderr(self) -> str: 110 | """Subclasses should implement this method to report the stderr yielded 111 | by the interaction. This should be obtained from the result of 112 | ``self.raw_result()``.""" 113 | pass 114 | 115 | @abstractmethod 116 | def process_result(self, result: Any) -> Any: 117 | """Subclasses should override this, to transform a JSON-encoded result 118 | into the application-specific result. 119 | """ 120 | pass 121 | 122 | def process_error(self, exception: ArgoException) -> Exception: 123 | """Subclasses may override this to specialize exceptions to their own 124 | domain. The default implementation returns the exception unchanged. 125 | """ 126 | return exception 127 | 128 | 129 | class Command(Interaction): 130 | """A higher-level interface to a JSON RPC command that follows Argo 131 | conventions. 132 | 133 | In particular, for a non-error result, the actual result should be 134 | in the `answer` field of the `result` dictionary, and the 135 | resulting state should be in the `state` field. 136 | 137 | Subclasses should implement ``process_result``, which transforms a 138 | dictionary representation of a JSON answer object into the 139 | corresponding command's appropriate representation. 140 | """ 141 | 142 | def state(self) -> Any: 143 | """Return the protocol state after the command is complete if the 144 | command did not error, or the protocol state prior to the the command 145 | otherwise.""" 146 | res = self.raw_result() 147 | if 'error' in res: 148 | return self.init_state 149 | elif 'result' in res: 150 | return res['result']['state'] 151 | else: 152 | raise ValueError("Invalid result type from JSON RPC") 153 | 154 | def _result_and_state_and_out_err(self) -> Tuple[Any, Any, str, str]: 155 | res = self.raw_result() 156 | if 'error' in res: 157 | msg = res['error']['message'] 158 | error_data = None 159 | if 'data' in res['error'] and 'data' in res['error']['data']: 160 | error_data = res['error']['data']['data'] 161 | msg += " " + str(error_data) 162 | exception = ArgoException(res['error']['code'], 163 | msg, 164 | error_data, 165 | res['error']['data']['stdout'], 166 | res['error']['data']['stderr']) 167 | raise self.process_error(exception) 168 | elif 'result' in res: 169 | return (res['result']['answer'], 170 | res['result']['state'], 171 | res['result']['stdout'], 172 | res['result']['stderr']) 173 | else: 174 | raise ValueError("Invalid result type from JSON RPC") 175 | 176 | def result(self) -> Any: 177 | """Return the result of the command.""" 178 | return self.process_result(self._result_and_state_and_out_err()[0]) 179 | 180 | def stdout(self) -> str: 181 | """Return the stdout printed during the execution of the command.""" 182 | return self._result_and_state_and_out_err()[2] 183 | 184 | def stderr(self) -> str: 185 | """Return the stderr printed during the execution of the command.""" 186 | return self._result_and_state_and_out_err()[3] 187 | 188 | 189 | class Query(Interaction): 190 | """A higher-level interface to a JSON RPC query that follows Argo 191 | conventions. 192 | 193 | In particular, for a non-error result, the actual result should be 194 | in the ``answer`` field of the ``result`` dictionary. Because queries 195 | do not change the state, it will be returned unchanged. 196 | 197 | Subclasses should implement ``process_result``, which transforms a 198 | dictionary representation of a JSON answer object into the 199 | corresponding command's appropriate representation. 200 | 201 | """ 202 | 203 | def state(self) -> Any: 204 | """Return the state prior to the query, because queries don't change 205 | the state. 206 | """ 207 | return self.init_state 208 | 209 | def _result_and_out_err(self) -> Tuple[Any, str, str]: 210 | res = self.raw_result() 211 | if 'error' in res: 212 | msg = res['error']['message'] 213 | if 'data' in res['error']: 214 | msg += " " + str(res['error']['data']['data']) 215 | exception = ArgoException(res['error']['code'], 216 | msg, 217 | res['error'].get('data').get('data'), 218 | res['error']['data']['stdout'], 219 | res['error']['data']['stderr']) 220 | raise self.process_error(exception) 221 | elif 'result' in res: 222 | return (res['result']['answer'], 223 | res['result']['stdout'], 224 | res['result']['stderr']) 225 | else: 226 | raise ValueError("Invalid result type from JSON RPC") 227 | 228 | def result(self) -> Any: 229 | """Return the result of the query.""" 230 | return self.process_result(self._result_and_out_err()[0]) 231 | 232 | def stdout(self) -> str: 233 | """Return the stdout printed during the execution of the command.""" 234 | return self._result_and_out_err()[1] 235 | 236 | def stderr(self) -> str: 237 | """Return the stderr printed during the execution of the command.""" 238 | return self._result_and_out_err()[2] 239 | 240 | 241 | class Notification: 242 | """A representation of a concrete stateless interaction with the server. 243 | Applications should subclass this according to their needs. 244 | 245 | Subclasses should call the superclass constructor with the method 246 | name and the parameters. 247 | 248 | """ 249 | _method: str 250 | _params: Dict[str, Any] 251 | 252 | def __init__(self, method: str, params: Dict[str, Any], 253 | connection: HasServerConnection) -> None: 254 | self.connection = connection 255 | self._method = method 256 | self._params = params 257 | connection.server_connection.send_notification(self._method, self._params) 258 | 259 | -------------------------------------------------------------------------------- /python/tests/test_mutable_file_echo_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import subprocess 4 | import time 5 | import unittest 6 | import signal 7 | from typing import Optional, Tuple 8 | 9 | import argo_client.connection as argo 10 | 11 | dir_path = Path(os.path.dirname(os.path.realpath(__file__))) 12 | 13 | file_dir = dir_path.joinpath('test-data') 14 | 15 | if not file_dir.is_dir(): 16 | print('ERROR: ' + str(file_dir) + ' is not a directory!') 17 | assert(False) 18 | 19 | # Test the custom command line argument to load a file at server start 20 | hello_file = file_dir.joinpath('hello.txt') 21 | 22 | 23 | # What the response looks like when a state is not in cache/pinned on the server 24 | def bad_state_res(*,uid,state): 25 | r = {'error':{'data':{'stdout':None,'data':state,'stderr':None},'code':20,'message':'Unknown state ID'},'jsonrpc':'2.0','id':uid} 26 | return r 27 | 28 | 29 | def assertShow(self : unittest.TestCase, 30 | connection : argo.ServerConnection, 31 | state : Optional[str], 32 | expected : str, 33 | *, 34 | expected_equal : bool = True) -> Tuple[int, str]: 35 | """Send a `show` command from `state` and ensure it returns `expected`. 36 | 37 | Returns the request uid and state in a tuple.""" 38 | 39 | next_state = None 40 | uid = connection.send_query("show", {"state": state}) 41 | actual = connection.wait_for_reply_to(uid) 42 | self.assertIn('result', actual) 43 | if 'result' in actual: 44 | self.assertIn('state', actual['result']) 45 | if 'state' in actual['result']: 46 | next_state = actual['result']['state'] 47 | self.assertIn('answer', actual['result']) 48 | if 'answer' in actual['result']: 49 | self.assertIn('value', actual['result']['answer']) 50 | if 'value' in actual['result']['answer']: 51 | if expected_equal: 52 | self.assertEqual(actual['result']['answer']['value'], expected) 53 | else: 54 | self.assertNotEqual(actual['result']['answer']['value'], expected) 55 | return (uid, next_state) 56 | 57 | 58 | class MutableFileEchoTests(unittest.TestCase): 59 | 60 | @classmethod 61 | def setUpClass(self): 62 | p = subprocess.Popen( 63 | ["cabal", "run", "exe:mutable-file-echo-api", "--verbose=0", "--", "--max-occupancy", "2", "http", "/", "--port", "8080"], 64 | stdout=subprocess.PIPE, 65 | stdin=subprocess.DEVNULL, 66 | stderr=subprocess.PIPE, 67 | start_new_session=True) 68 | time.sleep(3) 69 | assert(p is not None) 70 | poll_result = p.poll() 71 | if poll_result is not None: 72 | print(poll_result) 73 | print(p.stdout.read()) 74 | print(p.stderr.read()) 75 | assert(poll_result is None) 76 | 77 | self.p = p 78 | self.other_c = argo.ServerConnection(argo.HttpProcess('http://localhost:8080/')) 79 | 80 | 81 | @classmethod 82 | def tearDownClass(self): 83 | os.killpg(os.getpgid(self.p.pid), signal.SIGKILL) 84 | super().tearDownClass() 85 | 86 | 87 | def test_subsequent_connection(self): 88 | c = argo.ServerConnection(argo.HttpProcess('http://localhost:8080/')) 89 | ## Positive tests -- make sure the server behaves as we expect with valid RPCs 90 | 91 | # [c] Check that their is nothing to show if we haven't loaded a file yet 92 | (prev_uid, state) = assertShow(self, c, state=None,expected='') 93 | 94 | # [c] load a file 95 | hello_file = file_dir.joinpath('hello.txt') 96 | self.assertTrue(False if not hello_file.is_file() else True) 97 | uid = c.send_command("load", {"file path": str(hello_file), "state": state}) 98 | actual = c.wait_for_reply_to(uid) 99 | self.assertTrue('result' in actual and 'state' in actual['result']) 100 | state = actual['result']['state'] 101 | expected = {'result':{'state':state,'stdout':'','stderr':'','answer':[]},'jsonrpc':'2.0','id':uid} 102 | self.assertEqual(actual, expected) 103 | self.assertNotEqual(uid, prev_uid) 104 | self.assertNotEqual(state, None) 105 | prev_uid = uid 106 | 107 | # [c] check the contents of the loaded file 108 | (prev_uid, state) = assertShow(self, c, state=state,expected='Hello World!\n') 109 | 110 | # [other_c] start a subsequent connection 111 | other_c = argo.ServerConnection(argo.HttpProcess('http://localhost:8080/')) 112 | 113 | # [other_c] check that the other connection has nothing to show if we haven't loaded our own file yet 114 | (other_prev_uid, other_state) = assertShow(self, other_c, state=None,expected='') 115 | 116 | 117 | # [other_c] load a file 118 | base_file = file_dir.joinpath('base.txt') 119 | other_uid = other_c.send_command("load", {"file path": str(base_file), "state": other_state}) 120 | actual = other_c.wait_for_reply_to(other_uid) 121 | self.assertTrue('result' in actual and 'state' in actual['result']) 122 | other_state = actual['result']['state'] 123 | expected = {'result':{'state':other_state,'stdout':'','stderr':'','answer':[]},'jsonrpc':'2.0','id':other_uid} 124 | self.assertEqual(actual, expected) 125 | self.assertNotEqual(other_uid, other_prev_uid) 126 | self.assertNotEqual(other_state, None) 127 | other_prev_uid = other_uid 128 | 129 | # [other_c] clear the loaded file 130 | other_uid = other_c.send_command("clear", {"state": other_state}) 131 | actual = other_c.wait_for_reply_to(other_uid) 132 | self.assertTrue('result' in actual and 'state' in actual['result']) 133 | cleared_state = actual['result']['state'] 134 | expected = {'result':{'state':cleared_state,'stdout':'','stderr':'','answer':[]},'jsonrpc':'2.0','id':other_uid} 135 | self.assertEqual(actual, expected) 136 | self.assertNotEqual(cleared_state, other_state) 137 | self.assertNotEqual(other_uid, other_prev_uid) 138 | 139 | # [c] check the contents of the loaded file again in the original connection 140 | (prev_uid, state) = assertShow(self, c, state=state,expected='Hello World!\n') 141 | 142 | 143 | class InterruptTests(unittest.TestCase): 144 | # Connection to server 145 | c = None 146 | # process running the server 147 | p = None 148 | 149 | @classmethod 150 | def setUpClass(self): 151 | p = subprocess.Popen( 152 | ["cabal", "run", "exe:mutable-file-echo-api", "--verbose=0", "--", "--max-occupancy", "2", "http", "/", "--port", "8083", "--file", str(hello_file)], 153 | stdout=subprocess.PIPE, 154 | stdin=subprocess.DEVNULL, 155 | stderr=subprocess.PIPE, 156 | start_new_session=True) 157 | time.sleep(3) 158 | assert(p is not None) 159 | poll_result = p.poll() 160 | if poll_result is not None: 161 | print(poll_result) 162 | print(p.stdout.read()) 163 | print(p.stderr.read()) 164 | assert(poll_result is None) 165 | 166 | self.p = p 167 | 168 | @classmethod 169 | def tearDownClass(self): 170 | os.killpg(os.getpgid(self.p.pid), signal.SIGKILL) 171 | super().tearDownClass() 172 | 173 | # to be implemented by classes extending this one 174 | def test_interrupts(self): 175 | c1 = argo.ServerConnection(argo.HttpProcess('http://localhost:8083/')) 176 | c2 = argo.ServerConnection(argo.HttpProcess('http://localhost:8083/')) 177 | # load a file 178 | hello_file = file_dir.joinpath('hello.txt') 179 | self.assertTrue(False if not hello_file.is_file() else True) 180 | uid1 = c1.send_command("load", {"file path": str(hello_file), "state": None}) 181 | uid2 = c2.send_command("load", {"file path": str(hello_file), "state": None}) 182 | actual1 = c1.wait_for_reply_to(uid1) 183 | self.assertIn('result', actual1) 184 | self.assertIn('state', actual1['result']) 185 | state1 = actual1['result']['state'] 186 | actual2 = c2.wait_for_reply_to(uid2) 187 | self.assertIn('result', actual2) 188 | self.assertIn('state', actual2['result']) 189 | state2 = actual2['result']['state'] 190 | 191 | # simple sleep for 3 seconds 192 | t1 = time.time() 193 | uid1 = c1.send_query("sleep query", {"microseconds": 3000000, "state": state1}) 194 | actual1 = c1.wait_for_reply_to(uid1) 195 | t2 = time.time() 196 | self.assertIn('result', actual1) 197 | if 'result' in actual1: 198 | self.assertIn('state', actual1['result']) 199 | self.assertIn('answer', actual1['result']) 200 | if 'answer' in actual1['result']: 201 | self.assertIn('value', actual1['result']['answer']) 202 | if 'value' in actual1['result']['answer']: 203 | self.assertGreater(actual1['result']['answer']['value'], 2.9) 204 | self.assertGreater(t2 - t1, 2.9) 205 | 206 | 207 | # sleep/interrupt test 208 | newpid = os.fork() 209 | if newpid != 0: 210 | # parent tries to sleep 211 | one_hundred_sec = 100000000 212 | t1 = time.time() 213 | uid1 = c1.send_query("sleep query", {"microseconds": one_hundred_sec, "state": state1}) 214 | t2 = time.time() 215 | else: 216 | # child does not allow sleep 217 | time.sleep(3) 218 | uid2 = c2.send_notification("interrupt", {}) 219 | os._exit(0) 220 | # check interrupt actually interrupted 100sec sleep 221 | self.assertLess(t2 - t1, 10) 222 | 223 | # mutation/interrupt test 224 | newpid = os.fork() 225 | if newpid != 0: 226 | # parent tries to clean up 227 | two_sec = 2000000 228 | t1 = time.time() 229 | uid1 = c1.send_command("slow clear", {"pause microseconds": two_sec, "state": state1}) 230 | else: 231 | # child allows cleanup to start but not finish 232 | time.sleep(4) 233 | uid2 = c2.send_notification("interrupt", {}) 234 | os._exit(0) 235 | 236 | # check contents (N.B., the mutable server uses an IORef for the 237 | # contents, so the interrupt doesn't prevent effects.) 238 | (uid1, state1) = assertShow(self, c1, state=state1, expected='Hello World!\n', expected_equal = False) 239 | (uid1, state1) = assertShow(self, c1, state=state1, expected='', expected_equal = False) 240 | (uid2, state2) = assertShow(self, c2, state=state2, expected='Hello World!\n') 241 | -------------------------------------------------------------------------------- /file-echo-api/src/FileEchoServer.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE MultiParamTypeClasses #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | module FileEchoServer ( module FileEchoServer ) where 4 | 5 | import qualified Argo as Argo 6 | import qualified Argo.Doc as Doc 7 | import Control.Concurrent ( threadDelay ) 8 | import Control.Exception ( throwIO ) 9 | import Control.Monad.IO.Class ( liftIO ) 10 | import qualified Data.Aeson as JSON 11 | import Data.Aeson ( (.:), (.:?), (.=), (.!=) ) 12 | import Data.ByteString ( ByteString ) 13 | import qualified Data.ByteString.Char8 as Char8 14 | import qualified Data.Text as T 15 | import Data.Time.Clock.POSIX 16 | import Data.Scientific (scientific) 17 | import qualified System.Directory as Dir 18 | 19 | 20 | 21 | newtype FileContents = FileContents String 22 | 23 | data ServerState = ServerState 24 | { loadedFile :: Maybe FilePath 25 | -- ^ Loaded file (if any). 26 | , fileContents :: FileContents 27 | -- ^ Current file contents, or "" if one has not been loaded yet. 28 | } 29 | 30 | initialState :: 31 | Maybe FilePath -> 32 | (FilePath -> IO ByteString) -> 33 | IO ServerState 34 | initialState Nothing _reader = 35 | pure $ ServerState Nothing (FileContents "") 36 | initialState (Just path) reader = 37 | do contents <- FileContents . Char8.unpack <$> reader path 38 | pure $ ServerState (Just path) contents 39 | 40 | newtype ServerErr = ServerErr String 41 | newtype ServerRes a = ServerRes (Either ServerErr (a, FileContents)) 42 | newtype ServerCmd a = 43 | ServerCmd ((FilePath -> IO ByteString, FileContents) -> IO (ServerRes a)) 44 | 45 | 46 | ------------------------------------------------------------------------ 47 | -- Errors 48 | 49 | fileNotFound :: FilePath -> Argo.JSONRPCException 50 | fileNotFound fp = 51 | Argo.makeJSONRPCException 52 | 20051 (T.pack ("File doesn't exist: " <> fp)) 53 | (Just (JSON.object ["path" .= fp])) 54 | 55 | ------------------------------------------------------------------------ 56 | -- Load Command 57 | 58 | data LoadParams = LoadParams FilePath 59 | 60 | instance JSON.FromJSON LoadParams where 61 | parseJSON = 62 | JSON.withObject "params for \"load\"" $ 63 | \o -> LoadParams <$> o .: "file path" 64 | 65 | instance Doc.DescribedMethod LoadParams () where 66 | parameterFieldDescription = 67 | [("file path", 68 | Doc.Paragraph [Doc.Text "The file to read into memory."])] 69 | 70 | loadCmd :: LoadParams -> Argo.Command ServerState () 71 | loadCmd (LoadParams file) = 72 | do exists <- liftIO $ Dir.doesFileExist file 73 | if exists 74 | then do getFileContents <- Argo.getFileReader 75 | contents <- liftIO $ getFileContents file 76 | Argo.setState $ ServerState 77 | { loadedFile = Just file 78 | , fileContents = FileContents $ Char8.unpack contents 79 | } 80 | else Argo.raise (fileNotFound file) 81 | 82 | 83 | ------------------------------------------------------------------------ 84 | -- Prepend Command 85 | 86 | data PrependParams = PrependParams String 87 | 88 | instance JSON.FromJSON PrependParams where 89 | parseJSON = 90 | JSON.withObject "params for \"prepend\"" $ 91 | \o -> PrependParams <$> o .: "content" 92 | 93 | instance Doc.DescribedMethod PrependParams () where 94 | parameterFieldDescription = 95 | [("content", 96 | Doc.Paragraph [Doc.Text "The string to append to the left of the current file content on the server."])] 97 | 98 | prependCmd :: PrependParams -> Argo.Command ServerState () 99 | prependCmd (PrependParams str) = 100 | do (FileContents contents) <- fileContents <$> Argo.getState 101 | Argo.setState $ ServerState 102 | { loadedFile = Nothing 103 | , fileContents = FileContents $ str ++ contents 104 | } 105 | 106 | ------------------------------------------------------------------------ 107 | -- Drop Command 108 | 109 | data DropParams = DropParams Int 110 | 111 | instance JSON.FromJSON DropParams where 112 | parseJSON = 113 | JSON.withObject "params for \"drop\"" $ 114 | \o -> DropParams <$> o .: "count" 115 | 116 | instance Doc.DescribedMethod DropParams () where 117 | parameterFieldDescription = 118 | [("count", 119 | Doc.Paragraph [Doc.Text "The number of characters to drop from the left of the current file content on the server."])] 120 | 121 | dropCmd :: DropParams -> Argo.Command ServerState () 122 | dropCmd (DropParams n) = 123 | do (FileContents contents) <- fileContents <$> Argo.getState 124 | Argo.setState $ ServerState 125 | { loadedFile = Nothing 126 | , fileContents = FileContents $ drop n contents 127 | } 128 | 129 | ------------------------------------------------------------------------ 130 | -- Clear Command 131 | 132 | data ClearParams = ClearParams 133 | 134 | instance JSON.FromJSON ClearParams where 135 | parseJSON = 136 | JSON.withObject "params for \"show\"" $ 137 | \_ -> pure ClearParams 138 | 139 | instance Doc.DescribedMethod ClearParams () where 140 | parameterFieldDescription = [] 141 | 142 | clearCmd :: ClearParams -> Argo.Command ServerState () 143 | clearCmd _ = 144 | do Argo.setState $ ServerState 145 | { loadedFile = Nothing 146 | , fileContents = FileContents "" 147 | } 148 | 149 | ------------------------------------------------------------------------ 150 | -- Show Command 151 | 152 | data ShowParams = ShowParams 153 | { showStart :: Int 154 | -- ^ Inclusive start index in contents. 155 | , showEnd :: Maybe Int 156 | -- ^ Exclusive end index in contents. 157 | } 158 | 159 | instance JSON.FromJSON ShowParams where 160 | parseJSON = 161 | JSON.withObject "params for \"show\"" $ 162 | \o -> do start <- o .:? "start" .!= 0 163 | end <- o .:? "end" 164 | pure $ ShowParams start end 165 | 166 | instance Doc.DescribedMethod ShowParams JSON.Value where 167 | parameterFieldDescription = 168 | [ ("start", 169 | Doc.Paragraph [Doc.Text "Start index (inclusive). If not provided, the substring is from the beginning of the file."]) 170 | , ("end", Doc.Paragraph [Doc.Text "End index (exclusive). If not provided, the remainder of the file is returned."]) 171 | ] 172 | 173 | resultFieldDescription = 174 | [ ("value", 175 | Doc.Paragraph [ Doc.Text "The substring ranging from " 176 | , Doc.Literal "start", Doc.Text " to ", Doc.Literal "end" 177 | , Doc.Text "." ]) 178 | ] 179 | 180 | 181 | showCmd :: ShowParams -> Argo.Query ServerState JSON.Value 182 | showCmd (ShowParams start end) = 183 | do (FileContents contents) <- fileContents <$> Argo.getState 184 | let len = case end of 185 | Nothing -> length contents 186 | Just idx -> idx - start 187 | pure (JSON.object [ "value" .= (JSON.String $ T.pack $ take len $ drop start contents)]) 188 | 189 | 190 | ------------------------------------------------------------------------ 191 | -- Implode Query 192 | 193 | data ImplodeParams = ImplodeParams 194 | 195 | instance JSON.FromJSON ImplodeParams where 196 | parseJSON = 197 | JSON.withObject "params for \"implode\"" $ 198 | \_ -> pure ImplodeParams 199 | 200 | instance Doc.DescribedMethod ImplodeParams () where 201 | parameterFieldDescription = [] 202 | 203 | 204 | implodeCmd :: ClearParams -> Argo.Query ServerState () 205 | implodeCmd _ = liftIO $ throwIO Argo.internalError 206 | 207 | ---------------------------------------------------------------------- 208 | 209 | data Ignorable = 210 | ThisDatatype | ExistsTo | DemonstrateDocs 211 | 212 | instance JSON.FromJSON Ignorable where 213 | parseJSON (JSON.Bool True) = pure ThisDatatype 214 | parseJSON (JSON.Bool False) = pure ExistsTo 215 | parseJSON JSON.Null = pure DemonstrateDocs 216 | parseJSON _ = fail "Unknown value" 217 | 218 | instance Doc.Described Ignorable where 219 | typeName = "Ignorable data" 220 | description = 221 | [ Doc.Paragraph [Doc.Text "Data to be ignored can take one of three forms:"] 222 | , Doc.DescriptionList 223 | [ (pure $ Doc.Literal "true", 224 | Doc.Paragraph [Doc.Text "The first ignorable value"]) 225 | , (pure $ Doc.Literal "false", 226 | Doc.Paragraph [Doc.Text "The second ignorable value"]) 227 | , (pure $ Doc.Literal "null", 228 | Doc.Paragraph [Doc.Text "The ultimate ignorable value, neither true nor false"]) 229 | ] 230 | , Doc.Paragraph [Doc.Text "Nothing else may be ignored."] 231 | ] 232 | 233 | 234 | ------------------------------------------------------------------------ 235 | -- Ignore Command 236 | data IgnoreParams = IgnoreParams !Ignorable 237 | 238 | instance JSON.FromJSON IgnoreParams where 239 | parseJSON = 240 | JSON.withObject "params for \"ignore\"" $ 241 | \o -> IgnoreParams <$> o .: "to be ignored" 242 | 243 | instance Doc.DescribedMethod IgnoreParams () where 244 | parameterFieldDescription = 245 | [("to be ignored", 246 | Doc.Paragraph [Doc.Text "The value to be ignored goes here."])] 247 | 248 | ignoreCmd :: IgnoreParams -> Argo.Query ServerState () 249 | ignoreCmd _ = pure () 250 | 251 | 252 | ------------------------------------------------------------------------ 253 | -- Destroy State Command 254 | data DestroyStateParams = 255 | DestroyStateParams 256 | { 257 | stateToDestroy :: !Argo.StateID 258 | } 259 | 260 | instance JSON.FromJSON DestroyStateParams where 261 | parseJSON = 262 | JSON.withObject "params for \"destroy state\"" $ 263 | \o -> DestroyStateParams <$> o .: "state to destroy" 264 | 265 | instance Doc.DescribedMethod DestroyStateParams () where 266 | parameterFieldDescription = 267 | [("state to destroy", 268 | Doc.Paragraph [Doc.Text "The state to destroy in the server (so it can be released from memory)."]) 269 | ] 270 | 271 | destroyState :: DestroyStateParams -> Argo.Notification () 272 | destroyState (DestroyStateParams stateID) = Argo.destroyState stateID 273 | 274 | 275 | 276 | ------------------------------------------------------------------------ 277 | -- Destroy All States Command 278 | data DestroyAllStatesParams = DestroyAllStatesParams 279 | 280 | instance JSON.FromJSON DestroyAllStatesParams where 281 | parseJSON = 282 | JSON.withObject "params for \"destroy all states\"" $ 283 | \_ -> pure DestroyAllStatesParams 284 | 285 | instance Doc.DescribedMethod DestroyAllStatesParams () where 286 | parameterFieldDescription = [] 287 | 288 | 289 | destroyAllStates :: DestroyAllStatesParams -> Argo.Notification () 290 | destroyAllStates _ = Argo.destroyAllStates 291 | 292 | 293 | ------------------------------------------------------------------------ 294 | -- Sleep Query 295 | 296 | newtype SleepQueryParams = SleepQueryParams Int 297 | 298 | instance JSON.FromJSON SleepQueryParams where 299 | parseJSON = 300 | JSON.withObject "params for \"sleep query\"" $ 301 | \o -> SleepQueryParams <$> o .: "microseconds" 302 | 303 | instance Doc.DescribedMethod SleepQueryParams JSON.Value where 304 | parameterFieldDescription = 305 | [("microseconds", 306 | Doc.Paragraph [Doc.Text "The duration to sleep in microseconds."])] 307 | 308 | resultFieldDescription = 309 | [ ("value", 310 | Doc.Paragraph [ Doc.Text "Duration in seconds sleep lasted."]) 311 | ] 312 | 313 | sleepQuery :: SleepQueryParams -> Argo.Query ServerState JSON.Value 314 | sleepQuery (SleepQueryParams ms) = liftIO $ do 315 | t1 <- round `fmap` getPOSIXTime 316 | threadDelay ms 317 | t2 <- round `fmap` getPOSIXTime 318 | pure (JSON.object [ "value" .= (JSON.Number (scientific (t2 - t1) 0))]) 319 | 320 | ------------------------------------------------------------------------ 321 | -- Sleep Notification 322 | 323 | newtype SleepNotificationParams = SleepNotificationParams Int 324 | 325 | instance JSON.FromJSON SleepNotificationParams where 326 | parseJSON = 327 | JSON.withObject "params for \"sleep notification\"" $ 328 | \o -> SleepNotificationParams <$> o .: "microseconds" 329 | 330 | instance Doc.DescribedMethod SleepNotificationParams () where 331 | parameterFieldDescription = 332 | [("microseconds", 333 | Doc.Paragraph [Doc.Text "The duration to sleep in microseconds."])] 334 | 335 | sleepNotification :: SleepNotificationParams -> Argo.Notification () 336 | sleepNotification (SleepNotificationParams ms) = liftIO $ threadDelay ms 337 | 338 | ------------------------------------------------------------------------ 339 | -- Interrupt All Threads Command 340 | data InterruptAllThreadsParams = InterruptAllThreadsParams 341 | 342 | instance JSON.FromJSON InterruptAllThreadsParams where 343 | parseJSON = 344 | JSON.withObject "params for \"interrupt all threads\"" $ 345 | \_ -> pure InterruptAllThreadsParams 346 | 347 | instance Doc.DescribedMethod InterruptAllThreadsParams () where 348 | parameterFieldDescription = [] 349 | 350 | 351 | interruptAllThreads :: InterruptAllThreadsParams -> Argo.Notification () 352 | interruptAllThreads _ = Argo.interruptAllThreads 353 | 354 | 355 | ------------------------------------------------------------------------ 356 | -- SlowClear Command 357 | 358 | newtype SlowClear = SlowClear Int 359 | 360 | instance JSON.FromJSON SlowClear where 361 | parseJSON = 362 | JSON.withObject "params for \"slow clear\"" $ 363 | \o -> SlowClear <$> o .: "pause microseconds" 364 | 365 | instance Doc.DescribedMethod SlowClear () where 366 | parameterFieldDescription = 367 | [("pause microseconds", 368 | Doc.Paragraph [Doc.Text "The duration to sleep in microseconds between each character being cleared."])] 369 | 370 | slowClear :: SlowClear -> Argo.Command ServerState () 371 | slowClear (SlowClear ms) = do 372 | let go = do (FileContents contents) <- fileContents <$> Argo.getState 373 | case contents of 374 | [] -> Argo.modifyState $ \s -> s { loadedFile = Nothing } 375 | (_:cs) -> do 376 | Argo.modifyState $ \s -> s { fileContents = FileContents cs } 377 | liftIO $ threadDelay ms 378 | go 379 | go 380 | --------------------------------------------------------------------------------