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