Ind.ie is now Small Technology Foundation.
Commit 1d634374 authored by Aral Balkan's avatar Aral Balkan

Refactored to place all Pulse classes in the same source file to remove cyclical require issue.

parent 4cd82169
TODO: Replace with ind.ie/license when ready.
Copyright © 2015 Aral Balkan. © 2015 Ind.ie
All Rights Reserved.
\ No newline at end of file
######################################################################
#
# Ind.ie/pulse REST API client for Node.js.
#
# Uses restler under the hood and exposes
# the API via Bluebird promises.
#
# Example usage:
#
# Pulse = require('indie-pulse')
#
# pulse = new Pulse('<YOUR_API_KEY>')
#
# pulse.version()
# .then (result) ->
# console.log 'Version: ' + result
# .catch (error) ->
# console.log 'Error while attempting to get version: ' + error
#
# Copyright (c) 2014 Aral Balkan. Released under GNU AGPLv3
# Independence ★ Democracy ★ Design
# ❤ ind.ie
#
######################################################################
rest = require 'restler'
Promise = require 'bluebird'
util = require 'util'
printIt = require 'printit'
log = printIt {prefix: 'Pulse API', date: true}
#
# Public API
#
# log = (obj) ->
# # Nicer, more detailed logs
# console.log util.inspect(obj, showHidden=false, depth=5, colorize=true)
class PulseAPI
# The URL to pulse REST API.
_pulseBaseURL: 'http://localhost:8080/rest/'
_apiKey: null
_headers: null
constructor: (apiKey) ->
log.info "Creating a new Pulse API consumer with API key: #{apiKey}"
@_apiKey = apiKey
@_headers =
'Accept': '*/*'
'User-Agent': 'Pulse API consumer for node.js'
'X-API-Key': apiKey
######################################################################
#
# GET
#
######################################################################
getVersion: => @pulse('get', 'version')
# Data parameter:
# folder: folder ID
getModel: (data) => @pulse('get', 'model', data)
getConnections: => @pulse('get', 'connections')
# Data parameters:
# folder: folder ID
# device: device ID
getCompletion: (data) => @pulse('get', 'completion', data)
getConfig: => @pulse('get', 'config')
getConfigSync: => @pulse('get', 'config/sync')
getSystem: => @pulse('get', 'system')
getErrors: => @pulse('get', 'errors')
getDiscovery: => @pulse('get', 'discovery')
# Data parameter:
# id: device ID
getDeviceId: (data) => @pulse('get', 'deviceid', data)
######################################################################
#
# POST
#
######################################################################
# Data: same object structure as returned from GET:config
postConfig: (data) => @pulse('postJson', 'config', data)
postRestart: => @pulse('postJson', 'restart')
# Not sure about the format the data should be sent in: ask Jakob.
# postError: (data) -> @pulse('post', 'error', data)
postErrorClear: => @pulse('postJson', 'error/clear')
# Data: device (Device ID), addr (IP address:port)
postDiscoveryHint: (data) => @pulse('postJson', 'discovery/hint', data)
# TODO: Not working. Ask Jakob about this (see tests for more info)
postScan: (data) => @pulse('postJson', 'scan', data)
######################################################################
#
# Private functions.
#
######################################################################
#
# Helper function for creating REST URLS.
#_
_pulseURL: (endpoint) =>
return @_pulseBaseURL + endpoint
#
# Helper function that makes the calls.
#
pulse: (verb, url, data = {}) =>
# Sanity
if verb not in ['get', 'post', 'postJson']
throw new Error 'Unsupported REST verb: ' + verb
return new Promise (fulfill, reject) =>
retriesLeft = 3
# Choose the correct data token ('query' or 'data') based
# on whether the REST call is GET or POST, respectively.
restCall = null
optionsObject = {headers: @_headers}
if verb == 'get'
# GET
optionsObject.query = data
restCall = rest.get(@_pulseURL(url), optionsObject)
else if verb == 'postJson'
# POST JSON
if !data
data = {}
restCall = rest.postJson(@_pulseURL(url), data, optionsObject)
# else if verb == 'post'
# #
# # Regular POST
# # (The post error method, for example, requires the body of
# # the error in the message)
# #
# optionsObject.data = data
# restCall = rest.post(@_pulse(url), optionsObject)
restCall
.on 'success', (result) ->
# Success
# Inject the url and passed data as metadata on the result
# in case the handler needs to introspect it to differentiate this call from others.
console.log 'Pulse API RESULT: '
console.log result
# Some calls seems to return nothing (not an empty object but simply nothing)
# if there is nothing to return.
# TODO: Check with Jakob if this is expected behaviour. Surely this should
# ===== be handled differently by Pulse.
if !result
result = {}
result.__meta__ = {url: url, data: data}
process.nextTick ->
console.log "Pulse API call success:"
console.log result
fulfill(result)
.on 'fail', (data, response) ->
# A failure is a successful response with a failure code.
# Retrying will not alter the outcome so let’s fail.
data.__meta__ = {url: url, data: data}
console.log "Pulse API Fail:"
console.log url
console.log data
# log.info response
# log.info response.rawEncoded
if response.rawEncoded == 'CSRF Error\n'
log.error 'Check the Pulse GUI to make sure you have an API key set and check your code to make sure that you’re using it.'
process.nextTick ->
reject(data)
.on 'error', (error) ->
# An error is possibly recoverable — try to do so.
log.warn "#{error} while trying to reach #{url}"
if retriesLeft
retryOrRetries = 'retries'
if retriesLeft == 1
retryOrRetries = 'retry'
log.warn "#{retriesLeft} #{retryOrRetries} left. Trying again in 3 seconds…"
this.retry 3000
retriesLeft--
else
error.__meta__ = {url: url, data: data}
process.nextTick ->
log.error 'No retries left.'
reject(error)
module.exports = PulseAPI
This diff is collapsed.
################################################################################
#
# Ind.ie Pulse Process
#
# Starts and manages a pulse process. Informs the delegate of events.
#
# Usage: See test.coffee
#
# Copyright © 2014-2015, Aral Balkan.
# This is independent technology. See ind.ie/manifesto
# Released under the ind.ie/license
#
################################################################################
os = require 'os'
assert = require 'assert'
path = require 'path-extra'
spawn = require('child_process').spawn
exec = require('child_process').exec
printIt = require 'printit'
log = printIt {prefix: 'Pulse Process', date: true}
PulseConfig = require './PulseConfig'
class PulseProcess
@pulseProcess = null
#
# Pulse Process Delegate interface.
# (All delegate methods are optional.)
#
# pulseProcessRestApiIsReady
# pulseProcessFailedToStartProcess (data)
# pulseProcessDidSendData (data)
# pulseProcessDidSendErrorData (data)
# pulseProcessDidExitWithCode (data)
#
# A delegate is not required but you should really have one set
# to have control over the process.
#
delegate: {}
homeDirectory: null # e.g., NSHomeDirectory() as passed from Heartbeat native client
pulseDirectory: null # <homeDirectory>/Pulse
pulseConfigDirectory: null # <homeDirectory>/Pulse/Config
pulseSyncDirectory: null # <homeDirectory>/Pulse/Sync
pulseAPIKey: null
# The version of Pulse that is currently supported.
version: '1.0.0+14-gd2cd1f4'
constructor: (delegate={}, homeDirectory='') ->
# Sanity check: make sure that the version exists
assert(@version, 'Version must exist.')
@delegate = delegate
if delegate == {}
log.warn "There is not Pulse process delegate set."
# If we’re explicitly passed a home directory (as is the case with the
# sandboxed native OS X app, then use that. Otherwise, as is the case
# in Waystone), use the home directory on the system.
@homeDirectory = if (homeDirectory=='') then path.datadir() else homeDirectory
# Convenience paths
@pulseDirectory = path.join(@homeDirectory, 'Pulse')
@pulseConfigDirectory = path.join(@pulseDirectory, 'Config')
@pulseSyncDirectory = path.join(@pulseDirectory, 'Sync')
# Debug
# console.log 'Pulse process: home directory is ' + @homeDirectory
pid: =>
return @pulseProcess.pid
start: (generate=false, name='')=>
#
# Start Pulse.
#
#
# First shut down any existing instances.
#
console.log "Sending a shutdown message to any existing Pulse instances before starting up a new one."
PulseConfig.setHomeDirectory(@homeDirectory)
PulseConfig.getApiKey().then (apiKey) =>
@pulseAPIKey = apiKey
command = "curl -X POST --header \"X-API-Key: #{@pulseAPIKey}\" http://127.0.0.1:8080/rest/shutdown"
console.log "Command: #{command}"
exec command, (error, stdout, stderr) =>
console.log 'stdout: ' + stdout
console.log 'stderr: ' + stderr
if error != null
console.log 'exec error: ' + error
# OK, Now let’s start.
@_start generate, name
_start: (generate, name) =>
console.log "Pulse process._start(): generate: #{generate}, name: #{name}"
# Load the correct binary
# Currently supported: x64 Mac and Linux (more to be added)
supportedPlatforms = [
{platform: 'linux', architecture: 'x64', binary: 'linux-amd64'},
{platform: 'darwin', architecture: 'x64', binary: 'macosx-amd64'}
]
platform = os.platform()
architecture = os.arch()
binary = null
for supportedPlatform in supportedPlatforms
if (platform == supportedPlatform.platform) and (architecture == supportedPlatform.architecture)
binary = supportedPlatform.binary
# We can’t recover if the platform is not supported. Fail catastophically. (Shakespeare would be proud.)
assert.notEqual null, binary, 'No binary found for platform: ' + platform + ', architecture: ' + architecture + '. Bailing.'
binaryFilePath = __dirname + '/pulse/pulse-' + binary + '-' + @version + '/pulse'
#
# Launch the pulse process
#
pulseArguments = ['-no-browser']
if generate
pulseArguments.push("-generate=#{@pulseConfigDirectory}")
if name != ''
pulseArguments.push("-name=#{name}")
else
pulseArguments.push("-home=#{@pulseConfigDirectory}")
#
# Set the STNORESTART environment variable so that Pulse
# does not automatically restart.
# (I’m adding this in an effort to gain better control over
# the Pulse process as we seem unable to SIGTERM kill it.)
#
# TODO: Does this break anywhere where we are implicitly relying
# ===== on restarts. If so, we need to handle these manually.
#
# This might be making Pulse brittle after putting computer to sleep.
# Commenting out to test.
#
#env = process.env
#env['STNORESTART'] = 1
#options =
# 'env': env
log.info "About to start Pulse version #{@version} at home directory: #{@homeDirectory} with arguments: #{pulseArguments}"
@pulseProcess = spawn binaryFilePath, pulseArguments #, options
@pulseProcess.stdout.on 'data', (data) =>
#
# Generic data callback.
#
log.info "🌏 #{data}"
if data == '' then log.warn 'EMPTY DATA!'
if @delegate.pulseProcessDidSendData != undefined
@delegate.pulseProcessDidSendData(data)
#
# Specific callback for when the REST API is ready.
#
if /INFO: Starting web GUI/.test(data)
log.info '🌏 Pulse REST API is ready.'
if @delegate.pulseProcessRestApiIsReady != undefined
@delegate.pulseProcessRestApiIsReady()
else if /Is another copy of Pulse already running\?/.test(data)
log.error '🌏 Pulse was already running, this should not happen.'
@stop()
@pulseProcess.stderr.on 'data', (data) =>
#
# Generic error callback.
#
log.error "🌏😩 #{data}"
if @delegate.pulseProcessDidSendErrorData != undefined
@delegate.pulseProcessDidSendErrorData data
#
# Specific error callback when child process fails to start.
#
if /^execvp\(\)/.test(data)
log.error '🌏😱 Failed to start child process.'
if @delegate.pulseProcessFailedToStartProcess != undefined
@delegate.pulseProcessFailedToStartProcess data
@pulseProcess.on 'close', (code) =>
log.info '🌏👋 Exited with code ' + code
if @delegate.pulseProcessDidExitWithCode != undefined
@delegate.pulseProcessDidExitWithCode code
stop: =>
#
# Stop the Pulse process.
#
log.info '🌏 Killing Pulse process…'
@pulseProcess?.kill 'SIGTERM'
@pulseProcess = null
module.exports = PulseProcess
This diff is collapsed.
This diff is collapsed.
# indie-pulse
A promises-based API client for the [Ind.ie Pulse REST API][2] in Node.js.
## Installation
npm install indie-pulse
(For Heartbeat development, the module is cloned in from source.ind.ie by ./install)
## Usage
Pulse = require('indie-pulse')
pulse = new Pulse('<YOUR_API_KEY_HERE>')
pulse.version()
.then (result) ->
console.log 'Version: ' + result
.catch (error) ->
console.log 'Error while attempting to get version: ' + error
## Tests
coffee test.coffee
## Reference
* [The REST interface][2]
## Credits
* [Syncthing][1] by Jakob Borg, et. al.
* Uses [Restler][6] by Dan Webb.
* Uses [bluebird][7] by Petka Antonov.
Copyright &copy; 2014 [Aral Balkan][3]. Licensed under [GNU GPLv3][5]. Released with ❤ by [ind.ie][4]
[1]: http://syncthing.net
[2]: https://discourse.syncthing.net/t/the-rest-interface/85
[3]: https://aralbalkan.com
[4]: https://ind.ie
[5]: http://www.gnu.org/licenses/gpl-3.0.html
[6]: https://github.com/danwrong/restler
[7]: https://github.com/petkaantonov/bluebird
\ No newline at end of file
This diff is collapsed.
# Ind.ie Pulse Config
Configures Pulse. Used in Heartbeat and Waystone.
## Installation
Currently only via Git.
(In Heartbeat, it is installed by ./install)
## Example
See Heartbeat.
## Tests
None yet.
## Credits
* [Pulse][1] by Jakob Borg, et. al.
Copyright &copy; 2015 [Aral Balkan][2]. Licensed under [GNU AGPLv3][3]. Released with ❤ by [ind.ie][4]
[1]: http://labs.ind.ie
[2]: https://aralbalkan.com
[3]: http://www.gnu.org/licenses/agpl-3.0.html
[4]: https://ind.ie
\ No newline at end of file
......@@ -1020,7 +1020,6 @@ body .markdown-body
<pre><code>./install
</code></pre>
<p>Used in Heartbeat and Waystone.</p>
<h1 id="usage"><a name="user-content-usage" href="#usage" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Usage</h1>
<h2 id="pulse-process"><a name="user-content-pulse-process" href="#pulse-process" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Pulse Process</h2>
<h3 id="create-the-process-and-use-the-passed-homedirectory"><a name="user-content-create-the-process-and-use-the-passed-homedirectory" href="#create-the-process-and-use-the-passed-homedirectory" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Create the process and use the passed homeDirectory:</h3>
<pre><code>Pulse = require 'pulse-node'
......@@ -1030,16 +1029,16 @@ delegate =
// etc.
// …Other delegate methods.
pulseProcess = new Pulse.process(delegate, homeDirectory)
pulseProcess = new Pulse.Process(delegate, homeDirectory)
pulseProcess.start()
</code></pre>
<p>If homeDirectory is not provided, the data directory is used.</p>
<h3 id="generate-a-new-pulse-configuration"><a name="user-content-generate-a-new-pulse-configuration" href="#generate-a-new-pulse-configuration" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Generate a new Pulse configuration</h3>
<pre><code>pulseProcess = new Pulse.process(delegate, homeDirectory)
<pre><code>pulseProcess = new Pulse.Process(delegate, homeDirectory)
pulseProcess.start(generate=true, name='Waystone')
</code></pre>
<p>This would create a new Pulse configuration and call the device ‘Waystone’.</p>
<h2 id="example"><a name="user-content-example" href="#example" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Example</h2>
<h3 id="example"><a name="user-content-example" href="#example" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Example</h3>
<pre><code>Pulse = require 'pulse-process'
class TestPulseProcess
......@@ -1047,7 +1046,7 @@ class TestPulseProcess
pulseProcess = null
constructor: -&gt;
@pulseProcess = new Pulse.process(this)
@pulseProcess = new Pulse.Process(this)
@pulseProcess.start()
pulseProcessRestApiIsReady: -&gt;
......@@ -1075,12 +1074,29 @@ class TestPulseProcess
testPulseProcess = new TestPulseProcess()
</code></pre>
<h2 id="tests"><a name="user-content-tests" href="#tests" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Tests</h2>
<pre><code>coffee test.coffee
<h2 id="pulse-api"><a name="user-content-pulse-api" href="#pulse-api" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Pulse API</h2>
<h3 id="usage"><a name="user-content-usage" href="#usage" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Usage</h3>
<pre><code>Pulse = require 'pulse-node'
pulseAPI = new Pulse.API('&lt;YOUR_API_KEY_HERE&gt;')
pulseAPI.version()
.then (result) -&gt;
console.log 'Version: ' + result
.catch (error) -&gt;
console.log 'Error while attempting to get version: ' + error
</code></pre>
<h2 id="tests"><a name="user-content-tests" href="#tests" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Tests</h2>
<ul>
<li><code>coffee test.coffee</code></li>
<li><code>coffee test-api.coffee</code></li>
<li><code>coffee test-process.coffee</code></li>
<li>(No tests for Pulse Config yet — TODO.)</li>
</ul>
<h2 id="credits"><a name="user-content-credits" href="#credits" class="headeranchor-link" aria-hidden="true"><span class="headeranchor"></span></a>Credits</h2>
<ul>
<li><a href="http://syncthing.net">Pulse</a> by Jakob Borg, et. al.</li>
<li><a href="https://source.ind.ie/project/pulse-swift/tree/master">Pulse</a> is a fork of <a href="http://syncthing.net">Syncthing</a> by Jakob Borg, et. al.</li>
<li>Uses <a href="https://github.com/danwrong/restler">Restler</a> by Dan Webb.</li>
<li>Uses <a href="https://github.com/petkaantonov/bluebird">bluebird</a> by Petka Antonov.</li>
</ul>
<p>Copyright &copy; 2014, 2015 <a href="https://aralbalkan.com">Aral Balkan</a>. Licensed under Closed Source until Release. Released with ❤ by <a href="https://ind.ie">ind.ie</a></p>
<p>[3]: Closed Source until Release.</p></article></body></html>
\ No newline at end of file
<p>Copyright &copy; 2014, 2015 <a href="https://aralbalkan.com">Aral Balkan</a>. Licensed under the <a href="https://ind.ie/license">ind.ie/license</a>. Released with ❤ by <a href="https://ind.ie">ind.ie</a></p></article></body></html>
\ No newline at end of file
......@@ -2,16 +2,17 @@
Starts and manages an Ind.ie Pulse process. Includes configuration and API.
## Installation
./install
Used in Heartbeat and Waystone.
# Usage
## Pulse Process
### Create the process and use the passed homeDirectory:
Pulse = require 'pulse-node'
......@@ -21,19 +22,21 @@ Used in Heartbeat and Waystone.
// etc.
// …Other delegate methods.
pulseProcess = new Pulse.process(delegate, homeDirectory)
pulseProcess = new Pulse.Process(delegate, homeDirectory)
pulseProcess.start()
If homeDirectory is not provided, the data directory is used.
### Generate a new Pulse configuration
pulseProcess = new Pulse.process(delegate, homeDirectory)
pulseProcess = new Pulse.Process(delegate, homeDirectory)
pulseProcess.start(generate=true, name='Waystone')
This would create a new Pulse configuration and call the device ‘Waystone’.
## Example
### Example
Pulse = require 'pulse-process'
......@@ -42,7 +45,7 @@ This would create a new Pulse configuration and call the device ‘Waystone’.
pulseProcess = null
constructor: ->
@pulseProcess = new Pulse.process(this)
@pulseProcess = new Pulse.Process(this)
@pulseProcess.start()
pulseProcessRestApiIsReady: ->
......@@ -71,17 +74,41 @@ This would create a new Pulse configuration and call the device ‘Waystone’.
testPulseProcess = new TestPulseProcess()
## Pulse API
### Usage
Pulse = require 'pulse-node'
pulseAPI = new Pulse.API('<YOUR_API_KEY_HERE>')
pulseAPI.version()
.then (result) ->
console.log 'Version: ' + result
.catch (error) ->
console.log 'Error while attempting to get version: ' + error
## Tests
coffee test.coffee
* ```coffee test.coffee```
* ```coffee test-api.coffee```
* ```coffee test-process.coffee```
* (No tests for Pulse Config yet — TODO.)
## Credits
* [Pulse][1] by Jakob Borg, et. al.
* [Pulse][1] is a fork of [Syncthing][2] by Jakob Borg, et. al.
* Uses [Restler][3] by Dan Webb.
* Uses [bluebird][4] by Petka Antonov.
Copyright &copy; 2014, 2015 [Aral Balkan][2]. Licensed under Closed Source until Release. Released with ❤ by [ind.ie][4]
Copyright &copy; 2014, 2015 [Aral Balkan][5]. Licensed under the [ind.ie/license][6]. Released with ❤ by [ind.ie][7]
[1]: http://syncthing.net
[2]: https://aralbalkan.com
[3]: Closed Source until Release.
[4]: https://ind.ie
\ No newline at end of file
[1]: https://source.ind.ie/project/pulse-swift/tree/master
[2]: http://syncthing.net
[3]: https://github.com/danwrong/restler
[4]: https://github.com/petkaantonov/bluebird
[5]: https://aralbalkan.com
[6]: https://ind.ie/license
[7]: https://ind.ie
Pulse = require './index'
assert = require 'assert'
process = Pulse.process
api = Pulse.api
config = Pulse.config
Process = Pulse.Process
API = Pulse.API
Config = Pulse.Config
assert(Process.__name == 'PulseProcess', 'Pulse Process class name should be set.')
assert(API.__name == 'PulseAPI', 'Pulse API class name should be set.')
assert(Config.__name == 'PulseConfig', 'Pulse Config class name should be set.')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment