I am trying to create a node module based on node's internal process.stdio to have an object with stdin, stdout, stderr to use that is not tied to process in any way, to pass around and mock.
It its proving difficult to test this file for some reason. Even the minimal test of new Stdio() seems to "block" or "hang" jest. --forceExit works but there is some other weird or odd behavior. Is there anything specific about the code below that would cause the process to hang?
Here it is:
const tty = require('tty')
export function getStdout () {
var stdout
const fd = 1
stdout = new tty.WriteStream(fd)
stdout._type = 'tty'
stdout.fd = fd
stdout._isStdio = true
stdout.destroySoon = stdout.destroy
stdout._destroy = function (er, cb) {
// Avoid errors if we already emitted
er = er || new Error('ERR_STDOUT_CLOSE')
cb(er)
}
// process.on('SIGWINCH', () => stdout._refreshSize())
return stdout
}
export function getStderr () {
var stderr
const fd = 2
stderr = new tty.WriteStream(fd)
stderr._type = 'tty'
stderr.fd = fd
stderr._isStdio = true
stderr.destroySoon = stderr.destroy
stderr._destroy = function (er, cb) {
// Avoid errors if we already emitted
er = er || new Error('ERR_STDOUT_CLOSE')
cb(er)
}
// process.on('SIGWINCH', () => stderr._refreshSize())
return stderr
}
export function getStdin () {
var stdin
const fd = 0
stdin = new tty.ReadStream(fd, {
highWaterMark: 0,
readable: true,
writable: false
})
stdin.fd = fd
stdin.on('pause', () => {
if (!stdin._handle) { return }
stdin._readableState.reading = false
stdin._handle.reading = false
stdin._handle.readStop()
})
return stdin
}
export function setupStdio () {
var stdio = {}
Object.defineProperty(stdio, 'stdout', {
configurable: true,
enumerable: true,
get: getStdout
})
Object.defineProperty(stdio, 'stderr', {
configurable: true,
enumerable: true,
get: getStderr
})
Object.defineProperty(stdio, 'stdin', {
configurable: true,
enumerable: true,
get: getStdin
})
return stdio
}
export default class Stdio {
constructor () {
const {stdin, stderr, stdout} = setupStdio()
this.stdin = stdin
this.stderr = stderr
this.stdout = stdout
return this
}
}
This hangs the process in Jest, why?
import Stdio from './index'
test('works', () => {
const x = new Stdio()
expect(x).toBeTruthy()
})
Related
How can I write to the container's stdin, when using dockerode library? I tried doing it in a multiple ways, but nothing seems to work.
My current code that is not able to write to stdin:
export async function nameToStdName(
pluginName: string,
pluginDescription: string,
pluginId: number,
numberOfDuplicates: number
) {
const docker = new Docker();
const input = `${pluginName}; ${pluginDescription}`;
// Run the docker container and pass input from a string
const dockerImageName = 'name-to-stdname';
const dockerCmd = ['python', '/app/main.py', '-i', pluginId.toString(), '-v', numberOfDuplicates.toString()];
const options = {
cmd: dockerCmd,
AttachStdin: true,
AttachStdout: true,
Tty: false,
};
const container = await docker.createContainer({
Image: dockerImageName,
...options,
});
await container.start();
const stream = await container.attach({
stream: true,
stdin: true,
stdout: true,
});
// Handle output from container's stdout
let name = "";
stream.on('data', (data: Stream) => {
console.log(`Received output: ${data.toString()}`);
name += data.toString();
});
// Pass input to container's stdin
stream.write(input);
await container.wait();
return name;
}
I started building a desktop app using Electron Js that enable users to upload a folder that contains client side web app (either a native one with html, css, js files or other based on a framworke like Vue.js) and serve its content like what the browser do, in a new window after that the user click for ex on preview button. I used for that purpose a library called 'electron-serve', but each time I start the app and click the preview button nothing is shown (I expect to get a new window that show the served web app), and the following error is shown in the console of my vscode:
(node:12068) electron: Failed to load URL: app://-/ with error:
ERR_FILE_NOT_FOUND (Use electron --trace-warnings ... to show where
the warning was created) (node:12068)
UnhandledPromiseRejectionWarning: Error: ERR_FILE_NOT_FOUND (-6)
loading 'app://-/' at rejectAndCleanup
(node:electron/js2c/browser_init:165:7510) at
EventEmitter.failListener (node:electron/js2c/browser_init:165:7723)
at EventEmitter.emit (node:events:527:28) (node:12068)
UnhandledPromiseRejectionWarning: Unhandled promise rejection. This
error originated either by throwing inside of an async function
without a catch block, or by rejecting a promise which was not handled
with .catch(). To terminate the node process on unhandled promise
rejection, use the CLI flag --unhandled-rejections=strict (see
https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode).
And here is a minimal reproducible example of my project
main.js :
const { app, BrowserWindow, BrowserView, ipcMain, protocol } = require("electron");
const serve = require('electron-serve');
const path = require("path");
const url = require('url');
const isDiv = process.eventNames.NODE_ENV !== "production";
const isMac = process.platform === "darwin";
let loadURL = serve({ directory : path.join(__dirname, "./compressedFolder") });
let mainWindow;
// let view;
// create main window
function createMainWindow() {
mainWindow = new BrowserWindow({
icon: path.join(__dirname, "src/img/greenieweb.icns"),
width: 1200,
height: 800,
minHeight: 600,
minWidth: 1000,
webPreferences: {
title: "GreenieWeb",
preload: path.join(__dirname, "/src/preload/preload.js"),
devTools: true /*Disables Devtools*/,
nodeIntegration: true,
},
});
// open dev tools if in dev env
if (isDiv) {
mainWindow.webContents.openDevTools();
}
ipcMain.on('app-topreview-loaded', async (event) => {
const previewWindow = new BrowserWindow({
parent: mainWindow,
show: false,
width: 1200,
height: 800,
minHeight: 600,
minWidth: 1000,
webPreferences: {
title: "Preview",
},
})
await loadURL(previewWindow);
previewWindow.once('ready-to-show', () => {
previewWindow.show()
})
})
// load main html file
mainWindow.loadFile(path.join(__dirname, "./index.html"));
}
// App is ready
app.whenReady().then(() => {
// create the main window
createMainWindow();
// Disable menu
// Menu.setApplicationMenu(null);
// remove mainwindow from memory on close
mainWindow.on('closed', () => {
mainWindow = null;
});
// create window if none is created when app is active
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
});
renderer.js :
let previewBtn = document.getElementById("preview-btn");
const fs = versions.fs;
let gFRWRPCounter = 0;
let isEmptyDir = true
previewBtn.addEventListener("click", () => {
if(!isEmptyDir){
removeFolderFromDir()
}
addFolderToThePlace()
window.electronAPI.showWebApp()
})
function removeFolderFromDir(){
fs.rmSync('./compressedFolder', { recursive: true, force: true });
fs.mkdir("./compressedFolder", (err) => {
if(err){
console.log("Something went wrong")
}
})
isEmptyDir = true;
gFRWRPCounter = 0;
}
async function addFolderToThePlace(){
if(directoryHandle===undefined){
new Notification(emptyFolderNOTIFICATION_TITLE, {
body: emptyFolderNOTIFICATION_BODY,
}).show();
return;
}
for await (const file of getFilesRecursivelyWithRP("", directoryHandle)) {
const content = await file.text()
await fs.mkdir(
"./compressedFolder" + file.relativePath,
{ recursive: true },
(err) => {
if (err) {
console.log(err);
} else {
fs.writeFileSync(
"./compressedFolder" + file.relativePath + "/" + file.name,
content
);
}
}
);
}
isEmptyDir = false
}
async function* getFilesRecursivelyWithRP(fPath, entry) {
if (entry.kind === "file") {
const file = await entry.getFile();
if (file !== null) {
file.relativePath = fPath;
yield file;
}
} else if (entry.kind === "directory") {
let nFPath = "";
if(gFRWRPCounter>0){
nFPath = fPath + "/" + entry.name;
}else{
nFPath = fPath
gFRWRPCounter++;
}
for await (const handle of entry.values()) {
yield* getFilesRecursivelyWithRP(nFPath, handle);
}
}
}
preload.js :
const { contextBridge, ipcRenderer } = require("electron");
const fs = require('fs')
// this is just an example
contextBridge.exposeInMainWorld("versions", {
fs: fs,
});
contextBridge.exposeInMainWorld('electronAPI', {
showWebApp: () => ipcRenderer.send('app-topreview-loaded')
})
Help please.
Main/Handler:
// utils.js
// Export the callback
module.exports = {
...
installPythonRequirements: (venvPath) => {
return spawn(path.join(venvPath, 'bin', 'pip'), ['install', '-r', 'requirements.txt'], {cwd: path.join('.', 'py-darvester')});
}
}
// main.js
// Register a handler to the callback in utils.js
ipcMain.handle('config:install-pip-deps', (e, venvPath) => {installPythonRequirements(venvPath)})
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
...
installPythonRequirements: (venvPath) => ipcRenderer.invoke('config:install-pip-deps', venvPath)
});
What I've tried:
1) Use the raw return
// renderer (React.js)
...
// Call the handler and store its return
const venv_status = window.electronAPI.installPythonRequirements(venvPath);
// Determine the type of its return
console.log("venv_status", venv_status, typeof venv_status); // prints: "venv_status Promise object"
// Shouldn't it be ChildProcess type? I thought IPC handlers were always sync
// unless defined. Maybe I'm wrong so I tried async code for my next attempt...
venv_status.on('close', (code) => {...}); // throws: "TypeError: venv_status.on is not a function"
2) Use it as a promise
const venv_status = window.electronAPI.installPythonRequirements(venvPath).then((venv_status) => {
console.log("venv_status", venv_status, typeof venv_status); // venv_status is undefined
});
3) Registering on handlers inside utils.js
// utils.js
// Export the callback
module.exports = {
...
installPythonDeps: (venvPath) => {
const venv_status = spawn(path.join(venvPath, 'bin', 'pip'), ['install', '-r', 'requirements.txt'], {cwd: path.join('.', 'py-darvester')});
...
venv_status.on('close', (code) => {...}); // these handlers work, but I'm not sure how to send these to my renderer
}
}
How can I properly get the ChildProcess instance through Electron IPC? Is this possibly a security feature in Electron?
Solved by implementing a second IPC channel from main to renderer:
// utils.js - installPythonRequirements()
// For each `on` event, send relevant data through `webContents`
const venv_status = spawn(path.join(venvPath, 'bin', 'pip'), ['install', '-r', 'requirements.txt'], {cwd: path.join('.', 'py-darvester')});
const main_window = BrowserWindow.getAllWindows()[0];
venv_status.on('close', (code) => {
console.log('close', code.toString().trim());
main_window.webContents.send('utils:venv-status', {message: 'Python requirements installation closed', code: code, closed: true});
});
venv_status.on('error', (err) => {
console.log('err', err.toString().trim());
main_window.webContents('utils:venv-status', {message: err.message, code: err.code, closed: false});
});
venv_status.stdout.on('data', (data) => {
console.log('stdout', data.toString().trim());
main_window.webContents.send('utils:venv-status', {message: data.toString().trim(), code: null, closed: false});
});
venv_status.stderr.on('data', (data) => {
console.log('stderr', data.toString().trim());
main_window.webContents.send('utils:venv-status', {message: data.toString().trim(), code: 1, closed: false});
});
return {status: "Installing"} // oddly it doesn't reach this return though. return value is undefined
// preload.js
...
onVenvStatus: (callback) => ipcRenderer.on('utils:venv-status', callback)
...
// renderer
window.electronAPI.installPythonRequirements(data).then((venv_status) => {
window.electronAPI.onVenvStatus((_event, status) => {
setConsoleLines([...consoleLines, status.message]);
});
});
export const initForgeViewer = (urn: string, renderingHTMLElemet: HTMLElement): Promise<any> => {
const forgeOptions = getForgeOptions(urn)
return new Promise((resolve, reject) => {
Autodesk.Viewing.Initializer(forgeOptions, () => {
const viewerConfig = {
extensions: ["ToolbarExtension"],
sharedPropertyDbPath: undefined,
canvasConfig: undefined, // TODO: Needs documentation or something.
startOnInitialize: true,
experimental: []
}
const viewer = new Autodesk.Viewing.Private.GuiViewer3D(renderingHTMLElemet, viewerConfig)
const avd = Autodesk.Viewing.Document
viewer.setTheme('light-theme')
viewer.start()
avd.load(forgeOptions.urn, (doc: any) => { // Autodesk.Viewing.Document
const viewables = avd.getSubItemsWithProperties(doc.getRootItem(), { type: 'geometry', role: '3d' }, true)
if (viewables.length === 0) {
reject(viewer)
return
} else {
const initialViewable = viewables[0]
const svfUrl = doc.getViewablePath(initialViewable)
const modelOptions = { sharedPropertyDbPath: doc.getPropertyDbPath() }
viewer.loadModel(svfUrl, modelOptions, (model: any) => { // Autodesk.Viewing.Model
this.loadedModel = model
resolve(viewer)
})
}
})
})
})
}
I am using the above code to initialise Forge viewer. But I realise that Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT only emit at the first time I initialize the Forge viewer. If I clean the viewer in the following way and initialize it again. The OBJECT_TREE_CREATED_EVENT would be fired
this.viewer.finish()
this.viewer.removeEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT,this.onObjectTreeReady)
this.viewer = null
So I can assume you're completely destroying the viewer and creating it again, including all events, right? Please use the following:
viewer.tearDown()
viewer.finish()
viewer = null
Tested using v6
Is it possible to test the code below with Jasmine testing tool or any other npm module like rewire or similar?
const AuthValidatorDumb = require('./src/AuthValidatorDumb');
const AuthValidator = require('./src/AuthValidator');
const config = require('../config');
let instance;
if (!instance) {
if (config.get('auth.enabled')) {
instance = AuthValidator;
} else {
instance = AuthValidatorDumb;
}
}
module.exports = instance;
I've got a variant for testing the code above.Suppose you have:
1) The code for index.js in the question above.
2) AuthValidator.js:
class AuthValidator {}
module.exports = AuthValidator;
3) AuthValidatorDumb.js:
class AuthValidatorDumb {}
module.exports = AuthValidatorDumb;
Here is test/index.spec.js:
const proxyquire = require('proxyquire');
const AuthValidator = require('../src/AuthValidator');
const AuthValidatorDumb = require('../src/AuthValidatorDumb');
describe('auth index', () => {
it('should return AuthValidator', () => {
const configMock = { get: () => 'sth' };
const Instance = proxyquire('../index', {
'../config': configMock,
});
expect(new Instance() instanceof AuthValidator).toBeTruthy();
});
it('should return AuthValidatorDumb', () => {
const configMock = { get: () => undefined };
const Instance = proxyquire('../index', {
'../config': configMock,
});
expect(new Instance() instanceof AuthValidatorDumb).toBeTruthy();
});
});