-
Notifications
You must be signed in to change notification settings - Fork 154
[WIP] Windows Support #63
Changes from 2 commits
8dc4170
7ec59de
fd80771
3e74f21
88a9940
edfc88d
df57950
9a3ecd0
0e2e7a6
0f498a1
71c765b
c745b1d
c63ecc2
e8ed3f3
eca70f0
11ecf94
be63431
c1d30b8
4064534
f6d52e8
23c249b
8dccc95
7fafc97
7b698ce
e901267
96da92b
a2bd83d
e96384d
9d73f82
4526cf0
a1de56f
23770ca
610f5e8
a863284
83c727b
876e102
977a96b
90d4517
0fb299a
34c0f50
ecd609c
93e118c
f5f6dfb
07a391d
8595954
bfc7284
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: typo, should be |
||
const wasSuccessful = code === successfullCode || code === null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
|
@@ -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())); | ||
}); | ||
|
||
|
@@ -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'; | ||
|
@@ -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; | ||
} | ||
|
@@ -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`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
That way, the whole 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: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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', | ||
} | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ++, I think it should look something like:
And then |
||
// prettier-ignore | ||
const winDocumentsPath = winDocumentsRegRecord.split(' ')[winDocumentsRegRecord.split(' ').length - 1].replace('%USERPROFILE%\\', '').replace(/\s/g, ''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it'd be great to add a
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) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved |
||
|
||
// Change this boolean flag to skip project creation. | ||
// Useful when working on the flow, to avoid having to wait for a real project | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
||
|
@@ -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); | ||
} | ||
|
@@ -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); | ||
}); | ||
); | ||
}); | ||
}); | ||
}; | ||
|
@@ -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); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
There was a problem hiding this comment.
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 anenv
key.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 likels
orpwd
fail). Discussion here.At least, that was my experience with
spawn
. Given that, though, I'm happy to just have a ternary here :)There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
env
utility instead of callingnpm
directly. I'm gonna guess thatenv
is a linux/macOS exclusive utility, so that one probably won't work?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 avoidingshell: true
, for #25. So I'm fine if we handle this issue elsewhere.