I have written a small executable to start a WebSocket server for a Node development environment.
In a separate NodeJS application (running in Electron), I am using child_process.spawn() to run the binary and start the server on application start up.
This server child process hijacks the node child_process spawned command and doesn't exit, so I can't capture the exit code or handle failures. I just need to know that it started or didn't.
How do I start this process in Rust so that I can exit (on success, failure etc.) and the child keeps running?
The relevant parts of my main.rs:
extern crate ws;
use std::env;
use std::process::Command;
use ws::{connect, CloseCode};
fn main() {
let args: Vec<String> = env::args().collect();
let command = &args[1];
if command == "activate" {
println!("Activating");
stop_server_process();
start_server_process();
}
}
fn start_server_process() {
let mut command;
if cfg!(windows) {
command = Command::new(".\\node_modules\\.bin\\ws.cmd");
} else {
command = Command::new("./node_modules/.bin/ws");
}
command.arg("--websocket");
command.arg("test/mock-api/ws-server.js");
command.arg("-p");
command.arg("9401");
match command.spawn() {
Ok(child) => println!("Server process id is {}", child.id()),
Err(e) => {
println!("Server didn't start: {}", e);
std::process::exit(1);
},
}
std::process::exit(0);
}
fn stop_server_process() {
connect("ws://localhost:9401", |out| {
out.send("server-stop").unwrap();
move |msg| {
println!("Got message: {}", msg);
out.close(CloseCode::Normal)
}
}).unwrap()
}
I'm running the binary from my Electron app like so:
/**
* #return {Promise<Boolean>}
*/
static activate() {
return new Promise((resolve, reject) => {
const command = spawn(process.env.SERVER_LOCAL_PATH, ['activate']);
command.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
command.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
command.on('close', code => resolve(code === 0));
command.on('error', error => reject(error));
});
}
This never resolves as the ws websocket starts running and the spawned process is now picking up events from that. My console output is the following:
stdout: Activating <--- Rust binary output
Server process id is 78057 <--- Rust binary output
stderr: Serving at [various URL variations] <--- ws binary output
stdout: Client Connected <--- ws binary output
The WebSocket server is based on examples and code from this library. Here is a reproducable version of it:
module.exports = SocketBase => class Socket extends SocketBase {
websocket(wss) {
wss.on('connection', (ws) => {
console.log('Client Connected');
ws.on('message', (data) => {
if (data === 'server-stop') {
console.log('Stopping...');
process.exit(0);
}
// do some faked JSON response data
});
ws.on('close', () => {
console.log('Client Disconnected');
});
ws.on('error', (error) => {
console.log(error.toString());
});
});
}
};
Related
I am using the new version of Nuxt which does not come with a server folder
In the old version, you had a server folder and an index.js which contained the app
I want to add the websocket WS library with the new version of NuxtJS
The library requires an instance of server which you create by calling http.createServer(app) meaning an instance of the running express app. You would use this server instance now to listen to 3000 in the index.js file. Also create a sessionHandler which you obtain by calling session({}) with the express-session library
const WebSocket = require('ws')
function websocket({ server, sessionHandler, logger }) {
// https://github.com/websockets/ws/blob/master/examples/express-session-parse/index.js, if you pass a server instance here 'upgrade' handler will crash
const wss = new WebSocket.Server({ noServer: true })
function noop() {}
function heartbeat() {
this.isAlive = true
}
wss.on('connection', (ws, request, client) => {
ws.isAlive = true
ws.on('pong', heartbeat)
ws.on('message', (msg) => {
logger.info(`Received message ${msg} from user ${client}`)
ws.send(true)
})
})
server.on('upgrade', (request, socket, head) => {
sessionHandler(request, {}, () => {
logger.info(`${JSON.stringify(request.session)} WEBSOCKET SESSION PARSED`)
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request)
})
})
})
// TODO use a setTimeout here instead of a setInterval
setInterval(function ping() {
// wss.clients => Set
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate()
ws.isAlive = false
ws.ping(noop)
})
}, 30000)
return wss
}
module.exports = websocket
Does anyone know how I can make this work on the new Nuxt setup without the server folder
You can create a custom module and use nuxt hooks to get a server instance on listen event.
Create modules/ws.js:
const WebSocket = require('ws')
const wss = new WebSocket.Server({ noServer: true })
wss.on('connection', ws => {
ws.on('message', message => {
console.log('received: %s', message);
})
ws.send('Hello')
})
export default function () {
this.nuxt.hook('listen', server => {
server.on('upgrade', (request, socket, head) => {
wss.handleUpgrade(request, socket, head, ws => {
wss.emit('connection', ws);
})
})
})
}
And register the module in nuxt.config.js:
export default {
modules: [
'~/modules/ws'
]
}
In your case you could create a module directory instead of a single file to store multiple related files.
Create modules/ws/index.js
const websocket = require('./websocket') // this is your file
export default function () {
this.nuxt.hook('listen', server => {
websocket({
server,
sessionHandler (request, _, cb) { // example
cb()
},
logger: console // example
})
})
}
Then copy your file to modules/ws/websocket.js. You can use module.exports which is CommonJS format or change it into ES Module format, Nuxt(Webpack) can handle that.
In your code I notice that ws.send(true) cause an error TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type boolean (true) which basically mean you cannot send boolean.
i used to write pwa via vanilla javascript like this
importScripts('/src/js/idb.js');
importScripts('/src/js/utility.js');
const CACHE_STATIC_NAME = 'static-v4';
const CACHE_DYNAMIC_NAME = 'dynamic-v2';
const STATIC_FILES = [
'/',
'/index.html',
'/offline.html',
'/src/js/app.js',
'/src/js/feed.js',
'/src/js/promise.js',
'/src/js/fetch.js',
'/src/js/idb.js',
'/src/js/material.min.js',
'/src/css/app.css',
'/src/css/feed.css',
'/src/images/main-image.jpg',
'https://fonts.googleapis.com/css?family=Roboto:400,700',
'https://fonts.googleapis.com/icon?family=Material+Icons',
'https://cdnjs.cloudflare.com/ajax/libs/material-design-lite/1.3.0/material.indigo-pink.min.css'
];
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open(CACHE_STATIC_NAME)
.then(function(cache) {
console.log('[Service Worker] Installing Service Worker ...');
cache.addAll(STATIC_FILES);
})
);
});
self.addEventListener('activate', function(e) {
console.log('[Service Worker] Activating Service Worker ...');
// clear old cache
e.waitUntil(
caches.keys()
.then(function(cachedKeys) {
return Promise.all(cachedKeys.map(function(key) {
if(key !== CACHE_STATIC_NAME && key !== CACHE_DYNAMIC_NAME) {
return caches.delete(key);
}
}))
})
);
// Tell the active service worker to take control of the page immediately.
return self.clients.claim(); // to ensure that activating is correctly done
});
//After install, fetch event is triggered for every page request
self.addEventListener('fetch', function(event) {
let url = 'https://pwa-training-4a918.firebaseio.com/posts.json';
if(event.request.url === url) {
event.respondWith(
fetch(event.request).then(res => {
let clonedRes = res.clone();
// in order to clear ol data if new data is different from the original one
clearAllData('posts')
.then(() => {
return clonedRes.json()
})
.then(data => {
for(let key in data) {
writeData('posts', data[key])
}
});
return res;
})
);
// USE Cache only Strategy if the request is in the static Files
} else if(STATIC_FILES.includes(event.request.url)) {
event.respondWith(
caches.match(event.request)
);
} else {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(response => {
return caches.open(CACHE_DYNAMIC_NAME).then(cache => {
cache.put(event.request, response.clone());
return response;
})
})
})
.catch(err => {
return caches.open(CACHE_STATIC_NAME).then(cache => {
// i need to show offline page only if the failure is in the help Page
// because it does not make any sence if i show this page in case of the failure in files like css
if(event.request.headers.get('accept').includes('text/html')) {
return cache.match('/offline.html');
}
})
})
);
}
});
but when I'm trying to write my own in vuejs app I installed pwa via vue add pwa it created for me a file called registerServiceWorker.js that I don't understand because I'm not used to use it
This file contains the following
/* eslint-disable no-console */
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}
I don't know how to write my own pwa code here or where I can do that?
Also I don't know if it will work on localhost or not because from what I'm noticing it works in Production
So My Question is, How Can I Write PWA As I used to do with vanilla js in vue app? What are the steps should I do in order to accomplish my full custom PWA?
Can I Do That without using workbox?
if anyone can help me i'll be appreciated.
Thanks in advance.
I/(pretty sure most of us) won't likely throw to redo service worker from scratch in any project, Workbox is also recommended tools in Google Developers' page other than Vue CLI.
As the registerServiceWorker.js, that's boilerplate for your service worker cycle in your App, as the logs pretty straightforward in the flow of your app process
If you wanna to do from scratch still, i would suggest read https://developers.google.com/web/fundamentals/primers/service-workers/ to understand the fundamentals. I would recommend because service-worker pretty much "I hope you know what you doing with your app like what-when-to update/caching/do-when-offline/"
I have this cypress e2e tests.
/// <reference types="Cypress" />
describe('Toss Full Test', function () {
let polyfill;
const uuid = Cypress._.random(0, 1e6)
before(() => {
const polyfillUrl = 'https://unpkg.com/whatwg-fetch#3.0.0/dist/fetch.umd.js';
cy.request(polyfillUrl).then(response => {
polyfill = response.body;
});
});
Cypress.on('window:before:load', win => {
delete win.fetch;
win.eval(polyfill);
});
const SubscribeEmail = "tosstests" + uuid + "#yopmail.com";
const SubscribePassword = "tossTests123456!!";
const SubscribeLogin = "tosstests" + uuid;
it('Full process', function (win) {
cy.server();
cy.visit("/");
cy.route('POST', '/api/account/register').as('register');
//this could be lagging as ravendb is starting
cy.get("#LinkLogin", { timeout: 20000 }).click();
//register
cy.get("#LinkRegister").click();
cy.get("#NewEmail").type(SubscribeEmail);
cy.get("#NewName").type(SubscribeLogin);
cy.get("#NewPassword").type(SubscribePassword);
cy.get("#NewConfirmPassword").type(SubscribePassword);
cy.window()
.then(win => {
// disables runCaptcha
win.runCaptcha = new win.Function(['action'], 'return Promise.resolve(action)');
})
.then(
() => {
cy.get("#BtnRegister").click();
cy.wait('#register');
cy.get('#register').then(function (xhr) {
expect(xhr.status).to.eq(200);
});
}
);
})
})
The full code can be found here https://github.com/RemiBou/Toss.Blazor/tree/master/Toss.Tests.E2E.Cypress.
When I run my project with the following code
docker-compose up -V ravendb web
./node_modules/.bin/cypress open
The code runs well, the assertion "expect(xhr.status).to.eq(200);" returns true but the test execution never stops. Why is that ?
After a lot of trial and error I just needed to remove the "win" parameter from the method in the "if" call which was a test for getting a window reference. In fact this parameter is a callback you need to call at the end of the tests, I can't find any documentation about it.
Need gulp 4.0.2 task to wait for express 4.17.1 server to start, then open a browser.
Here's what I have that works (but I think it's pretty old school)...
// Start server in development or test mode
gulp.task('start:server', () => {
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
config = require(`./${serverPath}/config/environment`).default;
nodemon(`--inspect --trace-deprecation --trace-warnings -w ${serverPath} ${serverPath}`)
.on('log', onServerLog);
});
// Run nodemon with debugging (server/config/express.js runs webpack.make.js)
gulp.task('serve',
gulp.series(
gulp.parallel('eslint', 'eslint:tests', 'client:inject:scss', 'dist:client:assets:fonts', 'env:common'),
gulp.parallel('start:server', 'start:client'),
'watch'
)
);
// Perform HTTP GET to check for app readiness
function checkAppReady(cb) {
let options = {
host: 'localhost',
port: config.port
};
http.get(options, () => cb(true))
.on('error', () => cb(false));
}
// Check every 250ms until app server is ready
function whenServerReady(cb) {
let serverReady = false;
let appReadyInterval = setInterval(() =>
checkAppReady(ready => {
if(!ready || serverReady) {
return;
}
clearInterval(appReadyInterval);
serverReady = true;
cb();
}),
250);
}
// Wait until server is responding then open browser on client to our starting page
gulp.task('start:client', done => {
whenServerReady(() => {
opn(`http://localhost:${config.browserSyncPort}`/*, {app: 'google chrome'}*/);
done();
});
});
While this works well, I'm wondering if there's a more streamlined way to accomplish the same thing. The last StackOverflow post on the subject is more than 3 years old.
I'm using Express Graphql server with react native and Relay. My device does connects to the subscription but it does not subscribe to it. Here's my index.js on the server
const subscriptionServer = SubscriptionServer.create(
{
execute,
subscribe,
schema,
onOperation: (message, params, webSocket) => {
console.log(params)
return params;
},
onConnect: () => {
// My device does connects
console.log("client connected")
}
},
{
server,
path: '/subscriptions'
},
);
app.use('/graphql', graphqlHTTP({
schema,
graphiql: true
}));
app.use('/graphiql', graphiqlExpress({
endpointURL: '/graphql',
subscriptionsEndpoint: `ws://127.0.0.1:8080/subscriptions`
}));
server.listen(PORT, ()=> {
console.log("Groceries running on port " + PORT)
console.log(
`subscriptions is now running on ws://localhost:${PORT}/subscriptions'}`
);
});
The resolver for subscription on the server, it was quite troublesome to figure out since everyone is using executable schema from apolloGraphql.
export default {
type: OrderEdges,
args: {
ShopId: {type: GraphQLID},
},
subscribe: withFilter(() => pubsub.asyncIterator('orderConfirmed'), (payload, variables) => {
console.log(payload)
console.log(variables)
return payload.orderConfirmed.node.ShopId == variables.ShopId;
}),
}
Now the react-native client. My subscription setup with relay environment.
const setupSubscriptions = (config, variables, cacheConfig, observer) => {
const query = config.text; //does console logs the query
const subscriptionClient = new SubscriptionClient(`ws://192.168.0.100:8080/subscriptions`, {reconnect:true});
subscriptionClient.request({query, variables}, (err, result) => {
console.log(err) // doesn't get call inside the request method
observer.onNext(data:result)
})
}
My subscription method,
export default function() {
const variables = {
ShopId: shop.getShop()[0].id
}
requestSubscription(
environment,
{
subscription,
variables,
onCompleted: (res, err) => {
console.log(res)
console.log(err)
},
updater: (store) => {...},
onError: error => console.error(error),
onNext: (response) => {console.log(response)}
});
}
the component where I'm calling to subscribe,
import subscription from '../../GraphQLQueries/subscriptions/orderConfirmed';
class OrdersBox extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
//initializing subscription
orderSubscriptions();
}
When the device starts the app, my device is connected to the web socket as I can see the console.log statement inside the onConnect method in SubscriptionServer. But when the payload is published after a mutation, the subscribe method doesn't get called. I can't seem to figure out what I'm doing wrong. Maybe it's some react-native specific config that I'm missing cuz everything seems to work fine when I test it on graphiql.
I can't find any example of react-native and relay subscriptions used with express graphql.
note: Everything is working when I use subscription with graphiql. But not with react-native and relay.
Thanks in advance guys
....
I wasn't returning the subscriptionClient.request method. Adding a return statement solved the problem. You don't have to return when using subscribe method in subscriptions-transport-ws#0.8.3. But version 0.9.1 replaces the subscribe function with request which does require it to return.
try:
function setupSubscription(config, variables, cacheConfig, observer) {
const query = config.text;
const subscriptionClient = new SubscriptionClient(websocketURL, {
reconnect: true
});
const client = subscriptionClient.request({ query, variables }).subscribe({
next: result => {
observer.onNext({ data: result.data });
},
complete: () => {
observer.onCompleted();
},
error: error => {
observer.onError(error);
}
});
return {
dispose: client.unsubscribe
};
}
subscriptions-transport-ws#0.9.1