Skip to content
This repository has been archived by the owner on Jun 17, 2021. It is now read-only.

[WIP] Windows Support #63

Closed
wants to merge 46 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8dc4170
Add an initial color to Button
icarlossz Jul 15, 2018
7ec59de
add build step for linux
patcito Jul 15, 2018
fd80771
fix #48: get the repo url from package.json
karlsander Jul 15, 2018
3e74f21
Add default styles
icarlossz Jul 15, 2018
88a9940
Fix typo
icarlossz Jul 15, 2018
edfc88d
Initial changes
bennygenel Jul 16, 2018
df57950
removed console.log and added prettier-ignore
bennygenel Jul 16, 2018
9a3ecd0
Fix docs Guppy => Gatsby
chinanf-boy Jul 16, 2018
0e2e7a6
Add Chinese docs link
joshwcomeau Jul 16, 2018
0f498a1
Add Chat link
joshwcomeau Jul 16, 2018
71c765b
Merge pull request #67 from joshwcomeau/i18n-docs
joshwcomeau Jul 16, 2018
c745b1d
Merge pull request #64 from chinanf-boy/patch-1
joshwcomeau Jul 16, 2018
c63ecc2
fixes typo and missing isWin control, adds platform.services
bennygenel Jul 16, 2018
e8ed3f3
temporary solution for the eject error
bennygenel Jul 16, 2018
eca70f0
added missing declarations on platform.services
bennygenel Jul 16, 2018
11ecf94
Merge pull request #58 from karlsander/fix#48-broken-github-link
joshwcomeau Jul 16, 2018
be63431
Merge pull request #54 from icarlossz/patch-1
joshwcomeau Jul 17, 2018
c1d30b8
Quick refactor of style-fix for #53
joshwcomeau Jul 17, 2018
4064534
Merge pull request #55 from patcito/linux-build
joshwcomeau Jul 17, 2018
f6d52e8
Add action to make unsupported project errors fail gracefully
melanieseltzer Jul 17, 2018
23c249b
Merge pull request #75 from melanieseltzer/bugfix/importExistingProje…
karlsander Jul 17, 2018
8dccc95
add transparent b/w constants and use them for text (#69)
karlsander Jul 18, 2018
7fafc97
add Edit menu (#73)
karlsander Jul 18, 2018
7b698ce
Check for json.dependencies when running inferProjectType (#78)
melanieseltzer Jul 18, 2018
e901267
Quick tidy-up. Fix lint warnings
joshwcomeau Jul 18, 2018
96da92b
v0.1.0
joshwcomeau Jul 18, 2018
a2bd83d
Remove package-lock.json and update contributing.md to reflect change…
karlsander Jul 18, 2018
e96384d
Remove unused import (#61)
prashant-andani Jul 19, 2018
9d73f82
Tests for dependencies reducer (#76)
mik639 Jul 19, 2018
4526cf0
updated icon files (#89)
superhawk610 Jul 21, 2018
a1de56f
fix crash when deleting last dependency in list (#91)
superhawk610 Jul 21, 2018
23770ca
Add support for highlighting results (#83)
melanieseltzer Jul 21, 2018
610f5e8
Add icon instructions. Reorganize dev docs
joshwcomeau Jul 21, 2018
a863284
Update contribution docs (#93)
joshwcomeau Jul 21, 2018
83c727b
Merge pull request #95 from joshwcomeau/icon-instructions
superhawk610 Jul 21, 2018
876e102
Fix html display in dependency search, and styling tweaks for results…
melanieseltzer Jul 22, 2018
977a96b
Issue templates (#94)
joshwcomeau Jul 22, 2018
90d4517
Switch to individual issue templates (#99)
joshwcomeau Jul 22, 2018
0fb299a
add license and code of conduct (#98)
superhawk610 Jul 22, 2018
34c0f50
Initial changes
bennygenel Jul 16, 2018
ecd609c
removed console.log and added prettier-ignore
bennygenel Jul 16, 2018
93e118c
fixes typo and missing isWin control, adds platform.services
bennygenel Jul 16, 2018
f5f6dfb
temporary solution for the eject error
bennygenel Jul 16, 2018
07a391d
added missing declarations on platform.services
bennygenel Jul 16, 2018
8595954
redo task start commands
bennygenel Jul 22, 2018
bfc7284
Merge branch 'windows-support' of https://github.com/bennygenel/guppy…
bennygenel Jul 22, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"url": "https://github.com/joshwcomeau/guppy.git"
},
"scripts": {
"start": "NODE_ENV=development npm run start:react",
"start:react": "BROWSER=none node scripts/start.js",
"start": "cross-env NODE_ENV=development npm run start:react",
"start:react": "cross-env BROWSER=none node scripts/start.js",
"build": "node scripts/build.js",
"package:mac": "electron-packager . --platform=darwin --arch=x64 --icon=src/assets/icons/mac/logo.icns --prune=true --out=release-builds --overwrite",
"package": "GENERATE_SOURCEMAP=false npm run build && npm run package:mac",
Expand Down Expand Up @@ -49,6 +49,7 @@
"case-sensitive-paths-webpack-plugin": "2.1.1",
"chalk": "2.3.0",
"color": "3.0.0",
"cross-env": "^5.2.0",
"css-loader": "0.28.9",
"dotenv": "5.0.0",
"dotenv-expand": "4.2.0",
Expand Down Expand Up @@ -179,4 +180,4 @@
"icon": null,
"createdAt": 1529502079329
}
}
}
43 changes: 22 additions & 21 deletions scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,28 @@ let isElectronRunning = false;
* Prevents multiple re-runs of Electron App
*/
function runElectronApp() {
if (isElectronRunning)
return;
if (isElectronRunning) return;

isElectronRunning = true;
// For Windows Support
// Here cross-env can be used but
// since its not that big of a control I checked manually
// Maybe some env variable like `isWin` can be passed to the procces for easy use???
const command = /^win/.test(process.platform)
? `set ELECTRON_START_URL=http://localhost:${DEFAULT_PORT} && electron .`
: `ELECTRON_START_URL=http://localhost:${DEFAULT_PORT} electron .`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ternary could be avoided by passing the environment variable to exec (I believe you can pass an option object which can have an env key.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ternary could be avoided by passing the environment variable to exec (I believe you can pass an option object which can have an env key.

So there is indeed an option to specify env, but there appears to be a bug with Electron where not only are the values not passed, but it messes a bunch of stuff up (commands like ls or pwd fail). Discussion here.

At least, that was my experience with spawn. Given that, though, I'm happy to just have a ternary here :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshwcomeau Added a bounty on your SO question. Maybe we can get an answer for this issue there since the Github issue doesn't have any traction.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks a bunch for that! We got an answer that fixes the issue 🎉

I'm super not-clear how this will work on Windows, though. Essentially there are two proposed solutions:

  • Use the env utility instead of calling npm directly. I'm gonna guess that env is a linux/macOS exclusive utility, so that one probably won't work?
  • Supply the process.env.PATH to the command as an environment variable, to forward the current environment. Is PATH a thing on Windows?

That said, this may not even be an issue on Windows (the Electron bug with env might be MacOS only). And this is all a tangent anyway; the underlying issue is mostly about avoiding shell: true, for #25. So I'm fine if we handle this issue elsewhere.

exec(command, (err, stdout, stderr) => {
if (err) {
console.info(chalk.red('Electron app run failed: ') + stderr);
return;
}

exec(`ELECTRON_START_URL=http://localhost:${DEFAULT_PORT} electron .`,
(err, stdout, stderr) => {
if (err) {
console.info(chalk.red('Electron app run failed: ') + stderr);
return;
}

// Clear console for brevity
process.stdout.write('\x1bc');
// Clear console for brevity
process.stdout.write('\x1bc');

// Log output
console.info(stdout);
}
);
// Log output
console.info(stdout);
});
}

// Warn and crash if required files are missing
Expand Down Expand Up @@ -137,21 +140,19 @@ checkBrowsers(paths.appPath)
openBrowser(urls.localUrlForBrowser);
});

['SIGINT', 'SIGTERM'].forEach(function (sig) {
process.on(sig, function () {
['SIGINT', 'SIGTERM'].forEach(function(sig) {
process.on(sig, function() {
devServer.close();
process.exit();
});
});

/**
* Hook runElectronApp() to 'done' (compile) event
*
*
* Fails on error
*/
compiler.plugin('done',
stats => !stats.hasErrors() && runElectronApp()
);
compiler.plugin('done', stats => !stats.hasErrors() && runElectronApp());
})
.catch(err => {
if (err && err.message) {
Expand Down
81 changes: 56 additions & 25 deletions src/middlewares/task.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type { Task, ProjectType } from '../types';
const { ipcRenderer } = window.require('electron');
const childProcess = window.require('child_process');
const psTree = window.require('ps-tree');
const os = window.require('os');
const isWin = /^win/.test(os.platform());

export default (store: any) => (next: any) => (action: any) => {
if (!action.task) {
Expand Down Expand Up @@ -93,7 +95,10 @@ export default (store: any) => (next: any) => (action: any) => {
});

child.on('exit', code => {
const wasSuccessful = code === 0 || code === null;
// For Windows Support
// Windows sends code 1 (I guess its because we foce kill??)
const successfullCode = isWin ? 1 : 0;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: typo, should be successfulCode (only 1 l)

const wasSuccessful = code === successfullCode || code === null;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I think the issue here is that we lose the ability to detect true failures from false ones; if the task crashes, it would still display "Task Completed" for Windows.

That said, I don't see an easy solution to the problem, it doesn't look like taskkill supports specifying the exit code, and the current architecture means there's no clear way to communicate.

I don't think this is a showstopper; if we can get everything else working on Windows, we can deal with fixing this issue in a subsequent PR (likely by rearchitecting it so that ABORT_TASK handles this directly, instead of using the child-process listeners as an event bus)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think following up state of the process together with a custom redux variable might be beneficial. We can check if the termination was triggered with the switch or the process ended unexpectedly.

I don't know how much of this can be implemented but there can be lots of different informative states. For example rather than just having Running/Idle/Failed we can maybe have Started/Running/Stopping/Stopped/Idle/Failed/Ended Unexpectedly. Since Guppy targets new users, being more informative IMO a great approach.

const timestamp = new Date();

store.dispatch(completeTask(task, timestamp, wasSuccessful));
Expand Down Expand Up @@ -158,10 +163,12 @@ export default (store: any) => (next: any) => (action: any) => {
next(attachTaskMetadata(task, child.pid));

child.stdout.on('data', data => {
console.log('data stdout', data, task);
next(receiveDataFromTaskExecution(task, data.toString()));
});

child.stderr.on('data', data => {
console.log('data stdin', data, task);
next(receiveDataFromTaskExecution(task, data.toString()));
});

Expand All @@ -182,30 +189,15 @@ export default (store: any) => (next: any) => (action: any) => {
const { task } = action;
const { processId, name } = task;

// Our child was spawned using `shell: true` to get around a quirk with
// electron not working when specifying environment variables the
// "correct" way (see comment above).
//
// Because of that, `child.pid` refers to the `sh` command that spawned
// the actual Node process, and so we need to use `psTree` to build a
// tree of descendent children and kill them that way.
psTree(processId, (err, children) => {
if (err) {
console.error('Could not gather process children:', err);
}

const childrenPIDs = children.map(child => child.PID);

childProcess.spawn('kill', ['-9', ...childrenPIDs]);

if (isWin) {
// For Windows Support
// On Windows there is only one process so no need for psTree (see below)
// We use /f for focefully terminate process because it ask for confirmation
// We use /t to kill all child processes
// More info https://ss64.com/nt/taskkill.html
childProcess.spawn('taskkill', ['/pid', processId, '/f', '/t']);
ipcRenderer.send('removeProcessId', processId);

// Once the children are killed, we should dispatch a notification
// so that the terminal shows something about this update.
// My initial thought was that all tasks would have the same message,
// but given that we're treating `start` as its own special thing,
// I'm realizing that it should vary depending on the task type.
// TODO: Find a better place for this to live.
const abortMessage = isDevServerTask(name)
? 'Server stopped'
: 'Task aborted';
Expand All @@ -216,7 +208,43 @@ export default (store: any) => (next: any) => (action: any) => {
`\u001b[31;1m${abortMessage}\u001b[0m`
)
);
});
} else {
// Our child was spawned using `shell: true` to get around a quirk with
// electron not working when specifying environment variables the
// "correct" way (see comment above).
//
// Because of that, `child.pid` refers to the `sh` command that spawned
// the actual Node process, and so we need to use `psTree` to build a
// tree of descendent children and kill them that way.
psTree(processId, (err, children) => {
if (err) {
console.error('Could not gather process children:', err);
}

const childrenPIDs = children.map(child => child.PID);

childProcess.spawn('kill', ['-9', ...childrenPIDs]);

ipcRenderer.send('removeProcessId', processId);

// Once the children are killed, we should dispatch a notification
// so that the terminal shows something about this update.
// My initial thought was that all tasks would have the same message,
// but given that we're treating `start` as its own special thing,
// I'm realizing that it should vary depending on the task type.
// TODO: Find a better place for this to live.
const abortMessage = isDevServerTask(name)
? 'Server stopped'
: 'Task aborted';

next(
receiveDataFromTaskExecution(
task,
`\u001b[31;1m${abortMessage}\u001b[0m`
)
);
});
}

break;
}
Expand Down Expand Up @@ -261,9 +289,12 @@ const getDevServerCommand = (
projectType: ProjectType,
port: string
) => {
let command;
switch (projectType) {
case 'create-react-app':
return [`PORT=${port} npm`, 'run', task.name];
// For Windows Support
command = isWin ? `set PORT=${port} && npm` : `PORT=${port} npm`;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not urgent, but I think it'd be neat if we had a helper method, like formatCommandForPlatform, that looked like:

command = formatCommandForPlatform('npm`, { PORT: port });

That way, the whole set prefix (and even the whole prefixing with env variables) wouldn't need to be understood by devs making changes to any part of the code that starts commands.

Feel free to just add a TODO though, I can tackle this in a subsequent PR :)

return [command, 'run', task.name];
case 'gatsby':
return ['npm', 'run', task.name, '--', `-p ${port}`];
default:
Expand Down
28 changes: 24 additions & 4 deletions src/reducers/paths.reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import { ADD_PROJECT, IMPORT_EXISTING_PROJECT_FINISH } from '../actions';

import type { Action } from 'redux';

const childProcess = window.require('child_process');
const path = window.require('path');
const os = window.require('os');
const isWin = /^win/.test(os.platform());

type State = {
[projectId: string]: string,
Expand Down Expand Up @@ -42,12 +45,29 @@ export default (state: State = initialState, action: Action) => {
//
//
// Helpers
export const getDefaultParentPath = () =>
export const getDefaultParentPath = () => {
// Noticing some weird quirks when I try to use a dev project on the compiled
// "production" app, so separating their home paths should help.
process.env.NODE_ENV === 'development'
? `${os.homedir()}/guppy-projects-dev`
: `${os.homedir()}/guppy-projects`;

// For Windows Support
// Documents folder is much better place for project folders (Most programs use it as a default save location)
// Since there is a chance of being moved or users language might be differet we are reading the value from Registery
// There might be a better solution but this seems ok so far
const winDocumentsRegRecord = childProcess.execSync(
'REG QUERY "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders" /v Personal',
{
encoding: 'utf8',
}
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work on macOS? If not, it should be behind a conditional.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++, I think it should look something like:

const homedir = isWin
  ? getWindowsDocumentsPath()
  : os.homedir();

And then getWindowsDocumentsPath would hold the child process and subsequent data munging.

// prettier-ignore
const winDocumentsPath = winDocumentsRegRecord.split(' ')[winDocumentsRegRecord.split(' ').length - 1].replace('%USERPROFILE%\\', '').replace(/\s/g, '');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you happen to know how consistent windows registry formats are between versions of windows? Would this work from Windows 7 through to Windows 10, for example?

IIRC registry stuff tends not to change much, so I believe it's safe, but I haven't used Windows in over a decade 😅so my info is a bit rusty

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it a little bit. Initially split was creating bunch of empty values, that is why I replaced bunch of spaces. With single space split it should work in prior windows versions too. I'm going to try it on Win7 soon.

const homedir = isWin
? path.join(os.homedir(), winDocumentsPath)
: os.homedir();
return process.env.NODE_ENV === 'development'
? path.join(homedir, '/guppy-projects-dev')
: path.join(homedir, '/guppy-projects');
};

export const getDefaultPath = (projectId: string) =>
`${getDefaultParentPath()}/${projectId}`;
Expand Down
34 changes: 24 additions & 10 deletions src/services/create-project.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import type { ProjectType } from '../types';

const fs = window.require('fs');
const childProcess = window.require('child_process');
const path = window.require('path');
const os = window.require('os');
const isWin = /^win/.test(os.platform());
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be great to add a services/platform.services.js file that would export:

  • isWin, this quick check
  • getWindowsHomeDir, mentioned above
  • formatCommandForPlatform also mentioned above.

This is super non-urgent, I'm happy to go in after this merges and do it myself :) just mentioning it in case you're up for it (and so that I don't forget)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved isWin and getWindowsDir to platform.services.js. I was going to also propose a complete formatCommad service but I thought it's not really just for windows support so I think it should be in a separate PR. Using a global service function to get the command can benefit from future library additions like next. Doesn't require to change things on different files for example.


// Change this boolean flag to skip project creation.
// Useful when working on the flow, to avoid having to wait for a real project
Expand Down Expand Up @@ -62,9 +65,12 @@ export default (

const id = slug(projectName).toLowerCase();

const path = `${parentPath}/${id}`;
// For Windows Support
// To support cross platform with slashes and escapes
// Taken from https://github.com/AWolf81/guppy/commit/b2434d907d0c2c2585006d82ef14523a974de6a0
const projectPath = path.join(parentPath, id);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏🏻good catch @AWolf81


const [instruction, ...args] = getBuildInstructions(projectType, path);
const [instruction, ...args] = getBuildInstructions(projectType, projectPath);

const process = childProcess.spawn(instruction, args);

Expand All @@ -76,7 +82,7 @@ export default (
process.on('close', () => {
onStatusUpdate('Dependencies installed');

fs.readFile(`${path}/package.json`, 'utf8', (err, data) => {
fs.readFile(path.join(projectPath, 'package.json'), 'utf8', (err, data) => {
if (err) {
return console.error(err);
}
Expand All @@ -97,12 +103,16 @@ export default (

const prettyPrintedPackageJson = JSON.stringify(packageJson, null, 2);

fs.writeFile(`${path}/package.json`, prettyPrintedPackageJson, err => {
if (err) {
return console.error(err);
fs.writeFile(
path.join(projectPath, 'package.json'),
prettyPrintedPackageJson,
err => {
if (err) {
return console.error(err);
}
onComplete(packageJson);
}
onComplete(packageJson);
});
);
});
});
};
Expand Down Expand Up @@ -130,11 +140,15 @@ export const getBuildInstructions = (
projectType: ProjectType,
path: string
) => {
// For Windows Support
// Windows tries to run command as a script rather than on a cmd
// To force it we add *.cmd to the commands
const command = isWin ? 'npx.cmd' : 'npx';
switch (projectType) {
case 'create-react-app':
return ['npx', 'create-react-app', path];
return [command, 'create-react-app', path];
case 'gatsby':
return ['npx', 'gatsby', 'new', path];
return [command, 'gatsby', 'new', path];
default:
throw new Error('Unrecognized project type: ' + projectType);
}
Expand Down
10 changes: 8 additions & 2 deletions src/services/find-available-port.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@
* hard of a problem!
*/
const childProcess = window.require('child_process');

const os = window.require('os');
const MAX_ATTEMPTS = 15;

export default () =>
new Promise((resolve, reject) => {
const checkPort = (port = 3000, attemptNum = 0) => {
childProcess.exec(`lsof -i :${port}`, (err, res) => {
// For Windows Support
// Similar command to lsof
// Finds if the specified port is in use
const command = /^win/.test(os.platform())
? `netstat -aon | find "${port}"`
: `lsof -i :${port}`;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay! Glad to see an equivalent exists (looks like it goes all the way back to Vista too, nice)

childProcess.exec(command, (err, res) => {
// Ugh, childProcess assumes that no output means that there was an
// error, and `lsof` emits nothing when the port is empty. So,
// counterintuitively, an error is good news, and a response is bad.
Expand Down
Loading