UNPKG

thali

Version:
556 lines (509 loc) 29 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: NextGeneration/thaliMobile.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: NextGeneration/thaliMobile.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>'use strict'; var Promise = require('lie'); var thaliMobileNativeWrapper = require('thaliMobileNativeWrapper'); var thaliWifiInfrastructure = require('ThaliWifiInfrastructure')(); var EventEmitter = require('events'); /** @module thaliMobile */ /** * @file * * This is a convenience class to wrap together {@link * module:thaliMobileNativeWrapper} and {@link module:ThaliWifiInfrastructure} * in order to create a unified interface and set of events. This object assumes * that if it is being used then it has exclusive rights to call {@link * module:thaliMobileNativeWrapper}, {@link module:thaliMobileNative} and {@link * module:ThaliWifiInfrastructure}. */ /* METHODS */ /** * This object is our generic status wrapper that lets us return information * about both WiFi and Native. * * @public * @typedef {Object} combinedResult * @property {?Error} wifiResult * @property {?Error} nativeResult */ /** * This method MUST be called before any other method here other than * registering for events on the emitter. This method will call start on both * the {@link module:thaliMobileNativeWrapper} singleton and on an instance of * {@link module:ThaliWifiInfrastructure} class. Note that in the case of wifi * this call really does nothing but register the router object. In the case of * native however there is some setup work so an error is more meaningful. If an * error is received from start on either wifi or native then any subsequent * method calls below but stop will not attempt to interact with the failed * type. And yes, if both fail then essentially all the methods but stop turn * into NOOPs. * * This method also instructs the system to pay attention to network events. * If one or both radio types is not active but a network changed event * indicates that the relevant radio type is now active and if we are still in * the start state then this code MUST try to call start on the newly activated * radio stack. At that point if the object is in the start state for discovery * or advertising then we MUST also try to call the relevant start methods there * as well. * * This method is not idempotent. If called two times in a row without an * intervening stop a "Call Stop!" Error MUST be returned. * * This method can be called after stop since this is a singleton object. * * @public * @param {Object} router This is an Express Router object (for example, * express-pouchdb is a router object) that the caller wants non-TCP and WiFi * connections to be terminated with. This code will put that router at '/' so * make sure your paths are set up appropriately. If stop is called then the * system will take down the server so it is no longer available. * @returns {Promise&lt;module:thaliMobile~combinedResult>} */ module.exports.start = function (router) { return new Promise(); }; /** * This calls stop on both stacks even if start failed. * * Once called the object is in stop state. * * This method is idempotent and so MUST be able to be called multiple times * in a row without changing state. * * @public * @returns {Promise&lt;module:thaliMobile~combinedResult>} */ module.exports.stop = function () { return new Promise(); }; /** * This method calls the underlying startListeningForAdvertisements on * whichever radio stack is currently in start state. Note that once this method * is called it is giving explicit permission to this code to call this method * on a radio stack that is currently disabled when the method is called but is * later re-enabled due to a network changed event. In other words if {@link * module:thaliMobile.start} is called and say WiFi doesn't work. Then this * method is called and so advertising is only started for the non-TCP * transport. Then a network changed event happens indicating that WiFi is * available. Since we are still in start state this code will automatically * call {@link module:ThaliWifiInfrastructure~ThaliWifiInfrastructure#start} and * then will call {@link * module:ThaliWifiInfrastructure.startListeningForAdvertisements} because we * haven't yet called {@link module:thaliMobile.stopListeningForAdvertisements}. * If any of the calls triggered by the network event fail then the results MUST * be logged. * * This method is idempotent so multiple consecutive calls without an * intervening call to stop will not cause a state change. * * This method MUST NOT be called if the object is not in start state or a * "Call Start!" error MUST be returned. * * The combinedResult MUST return an error of "Not Active" for any radio type * that we did not call startListeningForAdvertisements on. * * @public * @returns {Promise&lt;module:thaliMobile~combinedResult>} */ module.exports.startListeningForAdvertisements = function () { return new Promise(); }; /** * This method calls the underlying stopListeningForAdvertisements on both * types regardless of which is in start state. * * This method is idempotent and MAY be called even if * startListeningForAdvertisements has not been called. * * @public * @returns {Promise&lt;module:thaliMobile~combinedResult>} */ module.exports.stopListeningForAdvertisements = function () { return new Promise(); }; /** * This method calls the underlying * startUpdateAdvertisingAndListening on whichever radio * stack is currently in start state. This method has the same behavior as * {@link module:thaliMobile.startListeningForAdvertisements} in that if a radio * type that was inactive should later become available and we are in start * state then we will try to call start and if that works and * stopAdvertisingAndListening has not been called then * we will try to call startUpdateAdvertisingAndListening on * the newly started stack. This includes the requirement to log any failed * attempts to call the various methods triggered by a network status change. * * Every call to this method is meant to change the current advertising flag * to indicate that new data is available. * * @public * @returns {Promise&lt;module:thaliMobile~combinedResult>} */ module.exports.startUpdateAdvertisingAndListening = function () { return new Promise(); }; /** * This method calls the underlying * stopAdvertisingAndListening on both types regardless of * which is in start state. * * This method is idempotent and MAY be called even if * startUpateAdvertisingAndListenForIncomingConnections has not been called. * * @public * @returns {Promise&lt;module:thaliMobile~combinedResult>} */ module.exports.stopAdvertisingAndListening = function () { return new Promise(); }; /* EVENTS */ /** * Enum to define the types of connections * * @readonly * @enum {string} */ module.exports.connectionTypes = { MULTI_PEER_CONNECTIVITY_FRAMEWORK: 'MPCF', BLUE_TOOTH: 'AndroidBlueTooth', TCP_NATIVE: 'tcp' }; /** * It is the job of this module to provide the most reliable guesses (and that * is what they are) as to when a peer is and is not available. The module MUST * err on the side of saying that a peer is available over saying they are not * available. * * The module MUST guarantee to its listeners (unlike {@link * module:thaliMobileNative}) that once it has fired a peerAvailabilityChanged * event for a specific peerIdentifier + connectionType combination that it will * not fire another peerAvailabilityChanged event for that peerIdentifier + * connectionType combination unless the combination has a new hostAddress or * portNumber. * * Note that this code explicitly does not do any kind of duplicate detection * for the same peerIdentifier across different connectionTypes. This is because * we want to surface to the caller all the ways a particular peer is available. * * This means that if this code receives multiple redundant notifications from * the various sources it is listening to about the same peerIdentifier and * connectionType combination then it must silently discard the duplicates. * * ## Android + BLE + Bluetooth * * When dealing with announcing that a peer has disappeared it is up to this * code to apply relevant heuristics. For example, in the case of Android it is * possible for a Thali app to go into the background. When this happens we * lower the power of the BLE status announcements but we don't change the * Bluetooth power. This means that a peer can seem to disappear when in fact * they are still in range of Bluetooth. This behavior is necessary to preserve * battery. This complicates things because the Android BLE stack never actually * says a peer is gone. Rather it sends a steady stream of announcements that * the peer is available and then stops when it doesn't see the peer anymore. So * this code has to notice those announcements and decide at what point after * they have stopped to declare the peer gone. In general we should experiment * with values like 30 seconds before deciding that a peer is gone. But why not * even longer? * * __Open Issue:__ How long should we wait after we don't hear any updates on * a peer being available over Android before we declare them gone? * * __Open Issue:__ A really obvious optimization would be to hook this code * into the {@link module:TCPServersManager} so it could see if we have a * Bluetooth or MPCF connection running with a particular peer. If we do then * obviously we wouldn't want to declare them gone even if we don't see them on * BLE or if MPCF told us they were gone. * * If an attempt to connect to a peer via Bluetooth whose presence we have * previously advertised via peerAvailabilityChanged should fail then we MUST * use our own heuristics to decide if we should mark the peer as no longer * available. Generally speaking if we still see the peer via BLE then we should * continue to mark them as available. Note that available doesn't mean * reachable. It is very easy for a peer to get overwhelmed with Bluetooth * requests and so be near but not able to accept a request. * * If we have been notified via a networkChanged event that either BLE or * Bluetooth is no longer available then we MUST mark all peers discovered via * BLE as no longer being present and send peerAvailabilityChanged events * reporting this status change. In theory we could make things more complex by * specifying that if BLE is turned off and Bluetooth isn't then we could * continue to say that the Bluetooth peers are there. But in practical terms it * isn't clear if this situation is possible (e.g. I suspect one can only turn * off Bluetooth, not sub-select between BLE and Bluetooth) and second it's more * complexity than we really need at this point. * * If we have been notified via a discoveryAdvertisingStatus event that * discovery is no longer available then we MUST treat existing discovered * Bluetooth peers as no longer being available and send peerAvailabilityChanged * events reporting this fact. Eventually we might get more flexible here and * allow Bluetooth peers to remain "available" and just use the connection * heuristics described previously to mark them as absent when necessary. But * for now let's keep things simple. * * If we have been notified via a discoveryAdvertisingStatus event that * advertising is no longer available this should not cause any change in our * information about peers we have discovered or will yet discover. * * ## iOS + MPCF * * In the case of the MPCF the native layer has an explicit lostPeer message. * But we aren't completely sure if it's totally reliable. So we either can * immediately declare the peer gone when we get lostPeer or we can take note of * the lostPeer message and then wait a bit before declaring the peer gone. Note * that we would only see this at the Node.js layer via a {@link * module:thaliMobileNativeWrapper~nonTCPPeerAvailabilityChanged} event. * * __Open Issue:__ How does MPCF deal with peers who have gone out of range? * Do they regularly send foundPeer messages or do they send one foundPeer * message and go quiet until they send a lostPeer message after a bit? * * __Open Issue:__ How reliable is the lostPeer message from MPCF? I believe * we have had a suspicion that even if we get a lostPeer we might still be able * to open a connection to that peer successfully (this would make sense if * discovery is over Bluetooth and data transfer over Wifi). But we need to now * make this concrete since we have to decide if we are going to put this * suspicion into our code. * * If an attempt to connect to a peer via MPCF whose presence we have previously * advertised via peerAvailabilityChanged should fail we MUST use our own * heuristics to decide if we should mark the peer as no longer available. * Typically we should allow several failed connection attempts before we decide * to mark the peer as no longer present. * * If we have been notified via a networkChanged event that one of Bluetooth or * Wifi are no longer available but not both then this should not cause any * immediate action since MPCF seems able to run exclusively over one transport * or the other. * * If however we have been notified via a networkChanged even that both * Bluetooth and Wifi are no longer available then we MUST mark all peers that * we have previously advertised as available as now being unavailable and * advertise this change via the peerAvailabilityChanged event. * * If we have been notified via a discoveryAdvertisingStatus event that * discovery is no longer available then we MUST change the availability of * peers discovered via MPCF as not available and advertise this fact via * peerAvailabilityChanged. * * __Open Issue:__ The MPCF explicitly states that one shouldn't keep discovery * on all the time. One suspects this is because they are using Bluetooth for * discovery. As such as may need to cycle discovery on and off to save battery. * In that case we will need to change the logic above so that turning off * discovery doesn't make the peer appear to be gone. * * If we have been notified via a discoveryAdvertisingStatus even that * advertising is no longer available then this should not cause any change in * our peer status. * * ## Wifi * * In the case of Wifi SSDP supports sending both announcements of * availability as well as announcements that one is going off line. If we * receive an announcement that a peer is going away then we can trust that * since it means the peer is deactivating its SSDP stack. But we can't rely on * receiving such an announcement since obviously if a peer just walks away * nothing will be said. So we MUST set a timer after receiving a SSDP * announcement for a peer (we will receive this via a {@link * module:ThaliWifiInfrastructure~wifiPeerAvailabilityChanged} event) and if we * don't hear an announcement that they are gone then we MUST automatically * generate such an announcement ourselves. * * Unlike the non-TCP transports, we do not use a proxy architecture with Wifi * and therefore do not know if an attempted connection succeeded or not. This * is why there is no equivalent requirement here to the ones above specifying * that failed connections should cause us to mark a peer as not available. * * If we have been notified via a networkChanged event that Wifi is no longer * available then all peers discovered via Wifi MUST be marked as not present * and this fact advertised via peerAvailabilityChanged. * * If we have been notified via a discoveryAdvertisingStatus event that * discovery is no longer available then we MUST change the availability of * peers discovered via Wifi to not available and advertise this fact via * peerAvailabilityChanged. This is a bit harsh since if Wifi is still working * we certainly could connect to those peers but once discovery is off we have * no way of knowing if the peer has disappeared and since we don't have a proxy * architecture we can't detect failed connections. So it's just easier for now * to just treat the peers as gone. * * If we have been notified via a discoveryAdvertisingStatus event that * advertising is no longer available then this should not cause any change in * our peer status. * * ## General guidelines on handling nonTCPPeerAvailabilityChanged and * wifiPeerAvailabilityChanged events * * If we receive a {@link * module:thaliMobileNativeWrapper~nonTCPPeerAvailabilityChanged} event then all * we have to do is return the arguments below as taken from the event with the * exception of setting hostAddress to 127.0.0.1 unless the peer is not * available in which case we should set both hostAddress and portNumber to * null. * * If we receive a {@link * module:ThaliWifiInfrastructure~wifiPeerAvailabilityChanged} event then all we * have to do is return the arguments below as taken from the event with the * exception of setting something reasonable for the suggestedTCPTimeout. * * @public * @event module:thaliMobile.event:peerAvailabilityChanged * @type {Object} * @property {string} peerIdentifier This is exclusively used to detect if * this is a repeat announcement or if a peer has gone to correlate it to the * announcement of the peer's presence. But this value is not used to establish * a connection to the peer, the hostAddress and portNumber handle that. * @property {string} hostAddress The IP/DNS address to connect to or null if * this is an announcement that the peer is no longer available. * @property {number} portNumber The port to connect to on the given * hostAddress or null if this is an announcement that the peer is no longer * available. * @property {number} suggestedTCPTimeout Provides a hint to what time out to * put on the TCP connection. For some transports a handshake can take quite a * long time. * @property {connectionTypes} connectionType Defines the kind of connection * that the request will eventually go over. This information is needed so that * we can better manage how we use the different transport types available to * us. */ thaliMobileNativeWrapper.on('nonTCPPeerAvailabilityChangedEvent', function (peer) { // Do stuff }); thaliWifiInfrastructure.on('wifiPeerAvailabilityChanged', function (hostAddress, portNumber) { // Do stuff }); /** * Fired whenever our state changes. * * @public * @typedef {Object} discoveryAdvertisingStatus * @property {boolean} discoveryActive If true indicates that our goal is to * have discovery active on all available radios. * @property {boolean} advertisingActive If true indicates that our goal is to * have advertising active on all available radios. * @property {boolean} nonTCPDiscoveryActive Indicates if discovery is active * on the non-TCP transport * @property {boolean} nonTCPAdvertisingActive Indicates if advertising is * active on the non-TCP transport * @property {boolean} wifiDiscoveryActive Indicates if discovery is active on * WiFi * @property {boolean} wifiAdvertisingActive Indicates if advertising is * active on WiFi */ /** * If we receive a {@link * module:thaliMobileNativeWrapper~discoveryAdvertisingStateUpdateNonTCPEvent} * there are a couple of possibilities: * - This just confirms whatever command we fired, we started advertising and * it is confirming that. We should only pass on this event the first time it * comes through. If we get multiple events with the same state and they all * match our current state then they should be suppressed. * - We thought something was started but now we are getting a notice that it * is stopped. In that case we need to internally record that discovery or * advertising is no longer started and fire this event to update. * - We thought something was stopped but now we are getting a notice that * they are started. This can happen because our network change event code * detected that a radio that was off is now on and we are trying to start * things. In that case we should mark the discovery or advertising as started * and fire this event. * * If we receive a {@link * module:ThaliWifiInfrastructure~discoveryAdvertisingStateUpdateWiFiEvent} then * the logic is effectively the same as above. * * A scary edge case is that we want discovery or advertising off and suddenly * the spontaneously turn on. This shouldn't happen (famous last words) so we'll * ignore it for the moment. * * @public * @event module:thaliMobile.event:discoveryAdvertisingStateUpdate * @type {Object} * @property {module:thaliMobile~discoveryAdvertisingStatus} * discoveryAdvertisingStatus */ thaliMobileNativeWrapper.on('discoveryAdvertisingStateUpdateNonTCPEvent', function (discoveryAdvertisingStateUpdateValue) { // Do stuff }); thaliWifiInfrastructure.on('discoveryAdvertisingStateUpdateWifiEvent', function (discoveryAdvertisingStateUpdateValue) { // Do stuff }); /** * Unless something went horribly wrong only one of thaliMobileNativeWrapper * or ThaliWifiInfrastructure should be enabled for this event at a time. We can * just pass the event value alone. * * @public * @event module:thaliMobile.event:networkChanged * @type {Object} * @property {module:thaliMobileNative~networkChanged} networkChangedValue */ thaliMobileNativeWrapper.on('networkChangedNonTCP', function (networkChangedValue) { // Do stuff }); thaliWifiInfrastructure.on('networkChangedWifi', function (networkChangedValue) { // Do stuff }); /** * If we receive a {@link * module:thaliMobileNativeWrapper~incomingConnectionToPortNumberFailed} and we * are in stop state then it means that we had a race condition where someone * tried to connect to the server just as we were killing it. If we get this in * the start state after having turned on advertising then it means that our * server has failed. The best we can do is try to close the server and open it * again. */ thaliMobileNativeWrapper.on('incomingConnectionToPortNumberFailed', function (portNumber) { // Do stuff }); /** * Use this emitter to subscribe to events * * @public * @fires module:thaliMobile.event:peerAvailabilityChanged * @fires module:thaliMobile.event:discoveryAdvertisingStateUpdate * @fires module:thaliMobile.event:networkChanged */ module.exports.emitter = new EventEmitter(); </code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-TCPServersManager.html">TCPServersManager</a></li><li><a href="module-thaliMobile.html">thaliMobile</a></li><li><a href="module-thaliMobileNative.html">thaliMobileNative</a></li><li><a href="module-thaliMobileNativeWrapper.html">thaliMobileNativeWrapper</a></li><li><a href="module-thaliNotificationAction.html">thaliNotificationAction</a></li><li><a href="module-thaliNotificationBeacons.html">thaliNotificationBeacons</a></li><li><a href="module-thaliNotificationClient.html">thaliNotificationClient</a></li><li><a href="module-thaliNotificationServer.html">thaliNotificationServer</a></li><li><a href="module-thaliPeerAction.html">thaliPeerAction</a></li><li><a href="module-thaliPeerDictionary.html">thaliPeerDictionary</a></li><li><a href="module-thaliPeerPoolInterface.html">thaliPeerPoolInterface</a></li><li><a href="module-ThaliWifiInfrastructure.html">ThaliWifiInfrastructure</a></li><li><a href="module-WifiBasedNativeMock.html">WifiBasedNativeMock</a></li></ul><h3>Externals</h3><ul><li><a href="external-_Mobile(_connect_)_.html">Mobile('connect')</a></li><li><a href="external-_Mobile(_discoveryAdvertisingStateUpdateNonTCP_)_.html">Mobile('discoveryAdvertisingStateUpdateNonTCP')</a></li><li><a href="external-_Mobile(_incomingConnectionToPortNumberFailed_)_.html">Mobile('incomingConnectionToPortNumberFailed')</a></li><li><a href="external-_Mobile(_killConnections_)_.html">Mobile('killConnections')</a></li><li><a href="external-_Mobile(_networkChanged_)_.html">Mobile('networkChanged')</a></li><li><a href="external-_Mobile(_peerAvailabilityChanged_)_.html">Mobile('peerAvailabilityChanged')</a></li><li><a href="external-_Mobile(_startListeningForAdvertisements_)_.html">Mobile('startListeningForAdvertisements')</a></li><li><a href="external-_Mobile(_startUpdateAdvertisingAndListening_)_.html">Mobile('startUpdateAdvertisingAndListening')</a></li><li><a href="external-_Mobile(_stopAdvertisingAndListening_)_.html">Mobile('stopAdvertisingAndListening')</a></li><li><a href="external-_Mobile(_stopListeningForAdvertisements_)_.html">Mobile('stopListeningForAdvertisements')</a></li></ul><h3>Classes</h3><ul><li><a href="ConnectionTable.html">ConnectionTable</a></li><li><a href="module-TCPServersManager-TCPServersManager.html">TCPServersManager</a></li><li><a href="module-thaliNotificationAction-NotificationAction.html">NotificationAction</a></li><li><a href="module-thaliNotificationBeacons-ParseBeaconsResponse.html">ParseBeaconsResponse</a></li><li><a href="module-thaliNotificationClient-ThaliNotificationClient.html">ThaliNotificationClient</a></li><li><a href="module-thaliNotificationServer-ThaliNotificationServer.html">ThaliNotificationServer</a></li><li><a href="module-thaliPeerAction-PeerAction.html">PeerAction</a></li><li><a href="module-thaliPeerDictionary-NotificationPeerDictionaryEntry.html">NotificationPeerDictionaryEntry</a></li><li><a href="module-thaliPeerDictionary-PeerConnectionInformation.html">PeerConnectionInformation</a></li><li><a href="module-thaliPeerDictionary-PeerDictionary.html">PeerDictionary</a></li><li><a href="module-thaliPeerPoolInterface-ThaliPeerPoolInterface.html">ThaliPeerPoolInterface</a></li><li><a href="module-ThaliWifiInfrastructure-ThaliWifiInfrastructure.html">ThaliWifiInfrastructure</a></li><li><a href="module-WifiBasedNativeMock-MobileCallInstance.html">MobileCallInstance</a></li><li><a href="module-WifiBasedNativeMock-WifiBasedNativeMock.html">WifiBasedNativeMock</a></li></ul><h3>Events</h3><ul><li><a href="module-thaliMobileNativeWrapper.html#~event:discoveryAdvertisingStateUpdateNonTCPEvent">discoveryAdvertisingStateUpdateNonTCPEvent</a></li><li><a href="module-ThaliWifiInfrastructure.html#~event:discoveryAdvertisingStateUpdateWifiEvent">discoveryAdvertisingStateUpdateWifiEvent</a></li><li><a href="module-TCPServersManager.html#~event:failedConnection">failedConnection</a></li><li><a href="module-thaliMobileNativeWrapper.html#~event:incomingConnectionToPortNumberFailed">incomingConnectionToPortNumberFailed</a></li><li><a href="module-thaliMobileNativeWrapper.html#~event:networkChangedNonTCP">networkChangedNonTCP</a></li><li><a href="module-ThaliWifiInfrastructure.html#~event:networkChangedWifi">networkChangedWifi</a></li><li><a href="module-thaliMobileNativeWrapper.html#~event:nonTCPPeerAvailabilityChangedEvent">nonTCPPeerAvailabilityChangedEvent</a></li><li><a href="module-TCPServersManager.html#~event:routerPortConnectionFailed">routerPortConnectionFailed</a></li><li><a href="module-ThaliWifiInfrastructure.html#~event:wifiPeerAvailabilityChanged">wifiPeerAvailabilityChanged</a></li><li><a href="module-thaliMobile.html#.event:event:discoveryAdvertisingStateUpdate">discoveryAdvertisingStateUpdate</a></li><li><a href="module-thaliMobile.html#.event:event:networkChanged">networkChanged</a></li><li><a href="module-thaliMobile.html#.event:event:peerAvailabilityChanged">peerAvailabilityChanged</a></li><li><a href="module-thaliNotificationAction-NotificationAction.html#.event:event:Resolved">Resolved</a></li><li><a href="module-thaliNotificationClient.html#.event:event:peerAdvertisesDataForUs">peerAdvertisesDataForUs</a></li></ul><h3>Global</h3><ul><li><a href="global.html#getPKCS12Content">getPKCS12Content</a></li><li><a href="global.html#getPublicKeyHash">getPublicKeyHash</a></li><li><a href="global.html#stopThaliReplicationManager">stopThaliReplicationManager</a></li><li><a href="global.html#ThaliEmitter">ThaliEmitter</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.4.0</a> on Mon Jan 18 2016 11:19:31 GMT+0200 (EET) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>