Merge pull request #108 from plebbit/master

Development
This commit is contained in:
plebeius.eth
2023-12-12 20:19:59 +01:00
committed by GitHub
14 changed files with 2402 additions and 46 deletions

98
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,98 @@
# docs https://github.com/marketplace/actions/create-release
# docs https://github.com/ncipollo/release-action
name: release
on:
push:
tags:
- 'v*'
jobs:
linux:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
# electron build
- uses: actions/checkout@v2
with:
# needed for git commit history changelog
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 16
- run: yarn install --frozen-lockfile
# make sure the ipfs executable is executable
- run: node electron/download-ipfs && sudo chmod +x bin/linux/ipfs
- run: CI='' yarn build
- run: yarn electron:build:linux
- run: ls dist
# publish version release
- run: node scripts/release-body > release-body.txt
- uses: ncipollo/release-action@v1
with:
artifacts: 'dist/seedit*.AppImage,dist/seedit-html*.zip'
token: ${{ secrets.GITHUB_TOKEN }}
replacesArtifacts: true
bodyFile: "release-body.txt"
allowUpdates: true
mac:
runs-on: macOS-latest
permissions:
contents: write
steps:
# electron build
- uses: actions/checkout@v2
with:
# needed for git commit history changelog
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 16
- run: yarn install --frozen-lockfile
# make sure the ipfs executable is executable
- run: node electron/download-ipfs && sudo chmod +x bin/mac/ipfs
- run: CI='' yarn build
- run: yarn electron:build:mac
- run: ls dist
# publish version release
- run: node scripts/release-body > release-body.txt
- uses: ncipollo/release-action@v1
with:
artifacts: 'dist/seedit*.dmg'
token: ${{ secrets.GITHUB_TOKEN }}
replacesArtifacts: true
bodyFile: "release-body.txt"
allowUpdates: true
windows:
runs-on: windows-latest
permissions:
contents: write
steps:
# electron build
- uses: actions/checkout@v2
with:
# needed for git commit history changelog
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 16
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn electron:build:windows
- run: dir dist
# publish version release
- run: node scripts/release-body > release-body.txt
- uses: ncipollo/release-action@v1
with:
artifacts: 'dist/seedit*.exe'
token: ${{ secrets.GITHUB_TOKEN }}
replacesArtifacts: true
bodyFile: "release-body.txt"
allowUpdates: true

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
# plebbit temp files
.plebbit
bin
# Logs
logs
*.log

View File

@@ -0,0 +1,46 @@
// hook that runs after electron-build
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const rootPath = path.resolve(__dirname, '..');
const distFolderPath = path.resolve(rootPath, 'dist');
const addPortableToPortableExecutableFileName = () => {
const files = fs.readdirSync(distFolderPath);
for (const file of files) {
if (file.endsWith('.exe') && !file.match('Setup')) {
const filePath = path.resolve(distFolderPath, file);
const renamedFilePath = path.resolve(
distFolderPath,
file.replace('seedit', 'seedit Portable')
);
fs.moveSync(filePath, renamedFilePath);
}
}
};
const createHtmlArchive = () => {
if (process.platform !== 'linux') {
return;
}
const { version } = require('../package.json');
const zipBinPath = path.resolve(rootPath, 'node_modules', '7zip-bin', 'linux', 'x64', '7za');
const seeditHtmlFolderName = `seedit-html-${version}`;
const outputFile = path.resolve(distFolderPath, `${seeditHtmlFolderName}.zip`);
const inputFolder = path.resolve(rootPath, 'build');
try {
// will break if node_modules/7zip-bin changes
execSync(`${zipBinPath} a ${outputFile} ${inputFolder}`);
// rename 'build' folder to 'seedit-html-version' inside the archive
execSync(`${zipBinPath} rn -r ${outputFile} build ${seeditHtmlFolderName}`);
} catch (e) {
e.message = 'electron build createHtmlArchive error: ' + e.message;
console.log(e);
}
};
exports.default = async (buildResult) => {
addPortableToPortableExecutableFileName();
createHtmlArchive();
};

90
electron/before-pack.js Normal file
View File

@@ -0,0 +1,90 @@
// download the ipfs binaries before building the electron clients
const fs = require('fs-extra');
const ProgressBar = require('progress');
const https = require('https');
const decompress = require('decompress');
const path = require('path');
const ipfsClientsPath = path.join(__dirname, '..', 'bin');
const ipfsClientWindowsPath = path.join(ipfsClientsPath, 'win');
const ipfsClientMacPath = path.join(ipfsClientsPath, 'mac');
const ipfsClientLinuxPath = path.join(ipfsClientsPath, 'linux');
// kubo download links https://docs.ipfs.tech/install/command-line/#install-official-binary-distributions
// plebbit kubu download links https://github.com/plebbit/kubo/releases
const ipfsClientVersion = '0.20.0';
const ipfsClientWindowsUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-windows-amd64`;
const ipfsClientMacUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-darwin-amd64`;
const ipfsClientLinuxPUrl = `https://github.com/plebbit/kubo/releases/download/v${ipfsClientVersion}/ipfs-linux-amd64`;
const downloadWithProgress = (url) =>
new Promise((resolve) => {
const split = url.split('/');
const fileName = split[split.length - 1];
const chunks = [];
const req = https.request(url);
req.on('response', (res) => {
// handle redirects
if (res.statusCode == 302) {
resolve(downloadWithProgress(res.headers.location));
return;
}
const len = parseInt(res.headers['content-length'], 10);
console.log();
const bar = new ProgressBar(` ${fileName} [:bar] :rate/bps :percent :etas`, {
complete: '=',
incomplete: ' ',
width: 20,
total: len,
});
res.on('data', (chunk) => {
chunks.push(chunk);
bar.tick(chunk.length);
});
res.on('end', () => {
console.log('\n');
resolve(Buffer.concat(chunks));
});
});
req.end();
});
const download = async (url, destinationPath) => {
let binName = 'ipfs';
if (destinationPath.endsWith('win')) {
binName += '.exe';
}
const binPath = path.join(destinationPath, binName);
// already downloaded, don't download again
if (fs.pathExistsSync(binPath)) {
return;
}
const split = url.split('/');
const fileName = split[split.length - 1];
const dowloadPath = path.join(destinationPath, fileName);
const file = await downloadWithProgress(url);
fs.ensureDirSync(destinationPath);
await fs.writeFile(binPath, file);
// decompress
// await fs.writeFile(dowloadPath, file);
// await decompress(dowloadPath, destinationPath);
// const extractedPath = path.join(destinationPath, 'kubo');
// const extractedBinPath = path.join(extractedPath, binName);
// fs.moveSync(extractedBinPath, binPath);
// fs.removeSync(extractedPath);
// fs.removeSync(dowloadPath);
};
const downloadIpfsClients = async () => {
await download(ipfsClientWindowsUrl, ipfsClientWindowsPath);
await download(ipfsClientMacUrl, ipfsClientMacPath);
await download(ipfsClientLinuxPUrl, ipfsClientLinuxPath);
};
exports.downloadIpfsClients = downloadIpfsClients
exports.default = async (context) => {
await downloadIpfsClients();
};

62
electron/build-docker.sh Executable file
View File

@@ -0,0 +1,62 @@
root_path=$(cd `dirname $0` && cd .. && pwd)
cd "$root_path"
# install node_modules
if [[ ! -f node_modules ]]
then
yarn || { echo "Error: failed installing 'node_modules' with 'yarn'" ; exit 1; }
fi
# download ipfs clients
node electron/download-ipfs || { echo "Error: failed script 'node electron/download-ipfs'" ; exit 1; }
dockerfile='
FROM electronuserland/builder:16
# install node_modules
WORKDIR /usr/src/seedit
COPY ./package.json .
COPY ./yarn.lock .
RUN yarn
# build native dependencies like sqlite3
RUN electron-builder install-app-deps
# copy source files
COPY ./bin ./bin
COPY ./electron ./electron
COPY ./src ./src
COPY ./public ./public
# required or yarn build fails
COPY ./.eslintrc.json ./.eslintrc.json
COPY ./.prettierrc ./.prettierrc
# react build
RUN yarn build
'
# build electron-builder docker image
# temporary .dockerignore to save build time
echo $'node_modules\ndist' > .dockerignore
echo "$dockerfile" | sudo docker build \
. \
--tag seedit-electron-builder \
--file -
rm .dockerignore
# build linux binary
sudo docker run \
--name seedit-electron-builder \
--volume "$root_path"/dist:/usr/src/seedit/dist \
--rm \
seedit-electron-builder \
yarn electron:build:linux
# build windows binary
sudo docker run \
--name seedit-electron-builder \
--volume "$root_path"/dist:/usr/src/seedit/dist \
--rm \
seedit-electron-builder \
yarn electron:build:windows

View File

@@ -0,0 +1,2 @@
const downloadIpfsClients = require('./before-pack').downloadIpfsClients;
downloadIpfsClients();

56
electron/log.js Normal file
View File

@@ -0,0 +1,56 @@
// require this file to log to file in case there's a crash
const envPaths = require('env-paths').default('plebbit', { suffix: false });
const util = require('util');
const fs = require('fs-extra');
const path = require('path');
// previous version created a file instead of folder
// we should remove this at some point
try {
if (fs.lstatSync(envPaths.log).isFile()) {
fs.removeSync(envPaths.log);
}
} catch (e) {}
const logFilePath = path.join(envPaths.log, new Date().toISOString().substring(0, 7));
fs.ensureFileSync(logFilePath);
const logFile = fs.createWriteStream(logFilePath, { flags: 'a' });
const writeLog = (...args) => {
logFile.write(new Date().toISOString() + ' ');
for (const arg of args) {
logFile.write(util.format(arg) + ' ');
}
logFile.write('\r\n');
};
const consoleLog = console.log;
console.log = (...args) => {
writeLog(...args);
consoleLog(...args);
};
const consoleError = console.error;
console.error = (...args) => {
writeLog(...args);
consoleError(...args);
};
const consoleWarn = console.warn;
console.warn = (...args) => {
writeLog(...args);
consoleWarn(...args);
};
const consoleDebug = console.debug;
console.debug = (...args) => {
// don't add date for debug because it's usually already included
for (const arg of args) {
logFile.write(util.format(arg) + ' ');
}
logFile.write('\r\n');
consoleDebug(...args);
};
// errors aren't console logged
process.on('uncaughtException', console.error);
process.on('unhandledRejection', console.error);
console.log(envPaths);

339
electron/main.js Normal file
View File

@@ -0,0 +1,339 @@
require('./log')
const {
app,
BrowserWindow,
Menu,
MenuItem,
Tray,
screen: electronScreen,
shell,
dialog,
nativeTheme
} = require('electron')
const isDev = require('electron-is-dev')
const path = require('path')
const startIpfs = require('./start-ipfs')
const startPlebbitRpcServer = require('./start-plebbit-rpc')
const { URL } = require('node:url')
const tcpPortUsed = require('tcp-port-used')
// retry starting ipfs every 10 second,
// in case it was started by another client that shut down and shut down ipfs with it
let startIpfsError
setInterval(async () => {
try {
const started = await tcpPortUsed.check(5001, '127.0.0.1')
if (started) {
return
}
await startIpfs()
}
catch (e) {
console.log(e)
startIpfsError = e
dialog.showErrorBox('IPFS error', startIpfsError.message)
}
}, 10000)
// use common user agent instead of electron so img, video, audio, iframe elements don't get blocked
// https://www.whatismybrowser.com/guides/the-latest-version/chrome
// https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome
// NOTE: eventually should probably fake sec-ch-ua header as well
let fakeUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
if (process.platform === 'darwin')
fakeUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
if (process.platform === 'linux')
fakeUserAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
const realUserAgent = `seedit/${require('../package.json').version}`
// add right click menu
const contextMenu = require('electron-context-menu')
contextMenu({
// prepend custom buttons to top
prepend: (defaultActions, parameters, browserWindow) => [
{
label: 'Back',
visible: parameters.mediaType === 'none',
enabled: browserWindow?.webContents?.canGoBack(),
click: () => browserWindow?.webContents?.goBack(),
},
{
label: 'Forward',
visible: parameters.mediaType === 'none',
enabled: browserWindow?.webContents?.canGoForward(),
click: () => browserWindow?.webContents?.goForward(),
},
{
label: 'Reload',
visible: parameters.mediaType === 'none',
click: () => browserWindow?.webContents?.reload(),
},
],
showLookUpSelection: false,
showCopyImage: true,
showCopyImageAddress: true,
showSaveImageAs: true,
showSaveLinkAs: true,
showInspectElement: true,
showServices: false,
showSearchWithGoogle: false,
})
const createMainWindow = () => {
let mainWindow = new BrowserWindow({
width: 1000,
height: 600,
show: false,
backgroundColor: nativeTheme.shouldUseDarkColors ? '#000000' : '#ffffff',
webPreferences: {
webSecurity: true, // must be true or iframe embeds like youtube can do remote code execution
nodeIntegration: false,
contextIsolation: true,
devTools: true, // TODO: change to isDev when no bugs left
preload: path.join(__dirname, 'preload.js'),
},
})
// set fake user agent
mainWindow.webContents.userAgent = fakeUserAgent
// set custom user agent and other headers for window.fetch requests to prevent origin errors
mainWindow.webContents.session.webRequest.onBeforeSendHeaders({urls: ['*://*/*']}, (details, callback) => {
const isIframe = !!details.frame?.parent
// if not a fetch request (or fetch request is from within iframe), do nothing, filtering webRequest by types doesn't seem to work
if (details.resourceType !== 'xhr' || isIframe) {
return callback({requestHeaders: details.requestHeaders})
}
// add privacy
details.requestHeaders['User-Agent'] = realUserAgent
details.requestHeaders['sec-ch-ua'] = undefined
details.requestHeaders['sec-ch-ua-platform'] = undefined
details.requestHeaders['sec-ch-ua-mobile'] = undefined
details.requestHeaders['Sec-Fetch-Dest'] = undefined
details.requestHeaders['Sec-Fetch-Mode'] = undefined
details.requestHeaders['Sec-Fetch-Site'] = undefined
// prevent origin errors
details.requestHeaders['Origin'] = undefined
callback({requestHeaders: details.requestHeaders})
})
// fix cors errors for window.fetch. must not be enabled for iframe or can cause remote code execution
mainWindow.webContents.session.webRequest.onHeadersReceived({urls: ['*://*/*']}, (details, callback) => {
const isIframe = !!details.frame?.parent
// if not a fetch request (or fetch request is from within iframe), do nothing, filtering webRequest by types doesn't seem to work
if (details.resourceType !== 'xhr' || isIframe) {
return callback({responseHeaders: details.responseHeaders})
}
// must delete lower case headers or both '*, *' could get added
delete details.responseHeaders['access-control-allow-origin']
delete details.responseHeaders['access-control-allow-headers']
delete details.responseHeaders['access-control-allow-methods']
delete details.responseHeaders['access-control-expose-headers']
details.responseHeaders['Access-Control-Allow-Origin'] = '*'
details.responseHeaders['Access-Control-Allow-Headers'] = '*'
details.responseHeaders['Access-Control-Allow-Methods'] = '*'
details.responseHeaders['Access-Control-Expose-Headers'] = '*'
callback({responseHeaders: details.responseHeaders})
})
const startURL = isDev
? 'http://localhost:3000'
: `file://${path.join(__dirname, '../build/index.html')}`
mainWindow.loadURL(startURL)
mainWindow.once('ready-to-show', async () => {
// make sure back button is disabled on launch
mainWindow.webContents.clearHistory()
mainWindow.show()
if (isDev) {
mainWindow.openDevTools()
}
if (startIpfsError) {
dialog.showErrorBox('IPFS error', startIpfsError.message)
}
})
mainWindow.on('closed', () => {
mainWindow = null
})
// don't open new windows
mainWindow.webContents.on('new-window', (event, url) => {
event.preventDefault()
mainWindow.loadURL(url)
})
// open links in external browser
// do not open links in seedit or will lead to remote execution
mainWindow.webContents.on('will-navigate', (e, originalUrl) => {
if (originalUrl != mainWindow.webContents.getURL()) {
e.preventDefault()
try {
// do not let the user open any url with shell.openExternal
// or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
// only open valid https urls to prevent remote execution
// will throw if url isn't valid
const validatedUrl = new URL(originalUrl)
let serializedUrl = ''
// make an exception for ipfs stats
if (validatedUrl.toString() === 'http://localhost:5001/webui/') {
serializedUrl = validatedUrl.toString()
} else if (validatedUrl.protocol === 'https:') {
// open serialized url to prevent remote execution
serializedUrl = validatedUrl.toString()
} else {
throw Error(`can't open url '${originalUrl}', it's not https and not the allowed http exception`)
}
shell.openExternal(serializedUrl)
} catch (e) {
console.warn(e)
}
}
})
// open links (with target="_blank") in external browser
// do not open links in seedit or will lead to remote execution
mainWindow.webContents.setWindowOpenHandler(({url}) => {
const originalUrl = url
try {
// do not let the user open any url with shell.openExternal
// or it will lead to remote execution https://benjamin-altpeter.de/shell-openexternal-dangers/
// only open valid https urls to prevent remote execution
// will throw if url isn't valid
const validatedUrl = new URL(originalUrl)
let serializedUrl = ''
// make an exception for ipfs stats
if (validatedUrl.toString() === 'http://localhost:5001/webui/') {
serializedUrl = validatedUrl.toString()
} else if (validatedUrl.protocol === 'https:') {
// open serialized url to prevent remote execution
serializedUrl = validatedUrl.toString()
} else {
throw Error(`can't open url '${originalUrl}', it's not https and not the allowed http exception`)
}
shell.openExternal(serializedUrl)
} catch (e) {
console.warn(e)
}
return {action: 'deny'}
})
// deny permissions like location, notifications, etc https://www.electronjs.org/docs/latest/tutorial/security#5-handle-session-permission-requests-from-remote-content
mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
// deny all permissions
return callback(false)
})
// deny attaching webview https://www.electronjs.org/docs/latest/tutorial/security#12-verify-webview-options-before-creation
mainWindow.webContents.on('will-attach-webview', (e, webPreferences, params) => {
// deny all
e.preventDefault()
})
if (process.platform !== 'darwin') {
// tray
const trayIconPath = path.join(
__dirname,
'..',
isDev ? 'public' : 'build',
'electron-tray-icon.png'
)
const tray = new Tray(trayIconPath)
tray.setToolTip('seedit')
const trayMenu = Menu.buildFromTemplate([
{
label: 'Open seedit',
click: () => {
mainWindow.show()
},
},
{
label: 'Quit seedit',
click: () => {
mainWindow.destroy()
app.quit()
},
},
])
tray.setContextMenu(trayMenu)
// show/hide on tray right click
tray.on('right-click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
// close to tray
if (!isDev) {
let isQuiting = false
app.on('before-quit', () => {
isQuiting = true
})
mainWindow.on('close', (event) => {
if (!isQuiting) {
event.preventDefault()
mainWindow.hide()
event.returnValue = false
}
})
}
}
const appMenuBack = new MenuItem({
label: '←',
enabled: mainWindow?.webContents?.canGoBack(),
click: () => mainWindow?.webContents?.goBack(),
})
const appMenuForward = new MenuItem({
label: '→',
enabled: mainWindow?.webContents?.canGoForward(),
click: () => mainWindow?.webContents?.goForward(),
})
const appMenuReload = new MenuItem({
label: '⟳',
role: 'reload',
click: () => mainWindow?.webContents?.reload(),
})
// application menu
// hide useless electron help menu
if (process.platform === 'darwin') {
const appMenu = Menu.getApplicationMenu()
appMenu.insert(1, appMenuBack)
appMenu.insert(2, appMenuForward)
appMenu.insert(3, appMenuReload)
Menu.setApplicationMenu(appMenu)
} else {
// Other platforms
const originalAppMenuWithoutHelp = Menu.getApplicationMenu()?.items.filter(
(item) => item.role !== 'help'
)
const appMenu = [appMenuBack, appMenuForward, appMenuReload, ...originalAppMenuWithoutHelp]
Menu.setApplicationMenu(Menu.buildFromTemplate(appMenu))
}
}
app.whenReady().then(() => {
createMainWindow()
app.on('activate', () => {
if (!BrowserWindow.getAllWindows().length) {
createMainWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

16
electron/preload.js Normal file
View File

@@ -0,0 +1,16 @@
const { contextBridge } = require('electron')
// dev uses http://localhost, prod uses file://...index.html
const isDev = window.location.protocol === 'http:'
const defaultPlebbitOptions = {
plebbitRpcClientsOptions: ['ws://localhost:9138']
}
contextBridge.exposeInMainWorld('defaultPlebbitOptions', defaultPlebbitOptions)
// expose a flag to indicate that we are running in electron
contextBridge.exposeInMainWorld('electron', { isElectron: true })
// uncomment for logs
// localStorage.debug = 'plebbit-js:*,plebbit-react-hooks:*,seedit:*'

48
electron/proxy-server.js Normal file
View File

@@ -0,0 +1,48 @@
// use this proxy server to debug ipfs api requests made by electron
const http = require('http');
const httpProxy = require('http-proxy');
// start proxy
const proxy = httpProxy.createProxyServer({});
// rewrite the request
proxy.on('proxyReq', function (proxyReq, req, res, options) {
// remove headers that could potentially cause an ipfs 403 error
proxyReq.removeHeader('X-Forwarded-For');
proxyReq.removeHeader('X-Forwarded-Proto');
proxyReq.removeHeader('sec-ch-ua');
proxyReq.removeHeader('sec-ch-ua-mobile');
proxyReq.removeHeader('user-agent');
proxyReq.removeHeader('origin');
proxyReq.removeHeader('sec-fetch-site');
proxyReq.removeHeader('sec-fetch-mode');
proxyReq.removeHeader('sec-fetch-dest');
proxyReq.removeHeader('referer');
});
proxy.on('error', (e) => {
console.error(e);
});
// start server
const start = ({ proxyPort, targetPort } = {}) => {
const server = http.createServer();
// never timeout the keep alive connection
server.keepAliveTimeout = 0;
server.on('request', async (req, res) => {
console.log(new Date().toISOString(), req.method, req.url, req.rawHeaders);
// fix cors error from dev url localhost:3000
// should not be necessary in production build using file url
res.setHeader('Access-Control-Allow-Origin', '*');
proxy.web(req, res, { target: `http://localhost:${targetPort}` });
});
server.on('error', console.error);
server.listen(proxyPort);
console.log(`proxy server listening on port ${proxyPort}`);
};
module.exports = { start };

110
electron/start-ipfs.js Normal file
View File

@@ -0,0 +1,110 @@
const isDev = require('electron-is-dev');
const path = require('path');
const { spawn } = require('child_process');
const fs = require('fs-extra');
const envPaths = require('env-paths').default('plebbit', { suffix: false });
const ps = require('node:process');
const proxyServer = require('./proxy-server');
// use this custom function instead of spawnSync for better logging
// also spawnSync might have been causing crash on start on windows
const spawnAsync = (...args) =>
new Promise((resolve, reject) => {
const spawedProcess = spawn(...args);
spawedProcess.on('exit', (exitCode, signal) => {
if (exitCode === 0) resolve();
else
reject(
Error(
`spawnAsync process '${spawedProcess.pid}' exited with code '${exitCode}' signal '${signal}'`
)
);
});
spawedProcess.stderr.on('data', (data) => console.error(data.toString()));
spawedProcess.stdin.on('data', (data) => console.log(data.toString()));
spawedProcess.stdout.on('data', (data) => console.log(data.toString()));
spawedProcess.on('error', (data) => console.error(data.toString()));
});
const startIpfs = async () => {
const ipfsFileName = process.platform == 'win32' ? 'ipfs.exe' : 'ipfs';
let ipfsPath = path.join(process.resourcesPath, 'bin', ipfsFileName);
let ipfsDataPath = path.join(envPaths.data, 'ipfs');
// test launching the ipfs binary in dev mode
// they must be downloaded first using `yarn electron:build`
if (isDev) {
let binFolderName = 'win';
if (process.platform === 'linux') {
binFolderName = 'linux';
}
if (process.platform === 'darwin') {
binFolderName = 'mac';
}
ipfsPath = path.join(__dirname, '..', 'bin', binFolderName, ipfsFileName);
ipfsDataPath = path.join(__dirname, '..', '.plebbit', 'ipfs');
}
if (!fs.existsSync(ipfsPath)) {
throw Error(`ipfs binary '${ipfsPath}' doesn't exist`);
}
console.log({ ipfsPath, ipfsDataPath });
fs.ensureDirSync(ipfsDataPath);
const env = { IPFS_PATH: ipfsDataPath };
// init ipfs client on first launch
try {
await spawnAsync(ipfsPath, ['init'], { env, hideWindows: true });
} catch (e) {}
// dont use 8080 port because it's too common
await spawnAsync(ipfsPath, ['config', '--json', 'Addresses.Gateway', "null"], {
env,
hideWindows: true,
});
// use different port with proxy for debugging during env
let apiAddress = '/ip4/127.0.0.1/tcp/5001';
if (isDev) {
apiAddress = apiAddress.replace('5001', '5002');
proxyServer.start({ proxyPort: 5001, targetPort: 5002 });
}
await spawnAsync(ipfsPath, ['config', 'Addresses.API', apiAddress], { env, hideWindows: true });
await new Promise((resolve, reject) => {
const ipfsProcess = spawn(
ipfsPath,
['daemon', '--migrate', '--enable-pubsub-experiment', '--enable-namesys-pubsub'],
{ env, hideWindows: true }
);
console.log(`ipfs daemon process started with pid ${ipfsProcess.pid}`);
let lastError;
ipfsProcess.stderr.on('data', (data) => {
lastError = data.toString();
console.error(data.toString());
});
ipfsProcess.stdin.on('data', (data) => console.log(data.toString()));
ipfsProcess.stdout.on('data', (data) => console.log(data.toString()));
ipfsProcess.on('error', (data) => console.error(data.toString()));
ipfsProcess.on('exit', () => {
console.error(`ipfs process with pid ${ipfsProcess.pid} exited`);
reject(Error(lastError));
});
process.on('exit', () => {
try {
ps.kill(ipfsProcess.pid);
} catch (e) {
console.log(e);
}
try {
// sometimes ipfs doesnt exit unless we kill pid +1
ps.kill(ipfsProcess.pid + 1);
} catch (e) {
console.log(e);
}
});
});
};
module.exports = startIpfs;

View File

@@ -0,0 +1,69 @@
const tcpPortUsed = require('tcp-port-used')
const {PlebbitWsServer} = require('@plebbit/plebbit-js/rpc')
const path = require('path')
const envPaths = require('env-paths').default('plebbit', { suffix: false })
const {randomBytes} = require('crypto')
const fs = require('fs-extra')
let isDev = true
try {
isDev = require('electron-is-dev')
} catch (e) {}
// PLEB, always run plebbit rpc on this port so all clients can use it
const port = 9138
const defaultPlebbitOptions = {
// find the user's OS data path
dataPath: !isDev ? envPaths.data : path.join(__dirname, '..', '.plebbit'),
ipfsHttpClientsOptions: ['http://localhost:5001/api/v0'],
// TODO: having to define pubsubHttpClientsOptions and ipfsHttpClientsOptions is a bug with plebbit-js
pubsubHttpClientsOptions: ['http://localhost:5001/api/v0'],
}
// generate plebbit rpc auth key if doesn't exist
const plebbitRpcAuthKeyPath = path.join(defaultPlebbitOptions.dataPath, 'auth-key')
let plebbitRpcAuthKey
try {
plebbitRpcAuthKey = fs.readFileSync(plebbitRpcAuthKeyPath, 'utf8')
}
catch (e) {
plebbitRpcAuthKey = randomBytes(32).toString('base64').replace(/[/+=]/g, '').substring(0, 40)
fs.ensureFileSync(plebbitRpcAuthKeyPath)
fs.writeFileSync(plebbitRpcAuthKeyPath, plebbitRpcAuthKey)
}
let pendingStart = false
const start = async () => {
if (pendingStart) {
return
}
pendingStart = true
try {
const started = await tcpPortUsed.check(port, '127.0.0.1')
if (started) {
return
}
const plebbitWebSocketServer = await PlebbitWsServer({port, plebbitOptions: defaultPlebbitOptions, authKey: plebbitRpcAuthKey})
console.log(`plebbit rpc: listening on ws://localhost:${port} (local connections only)`)
console.log(`plebbit rpc: listening on ws://localhost:${port}/${plebbitRpcAuthKey} (secret auth key for remote connections)`)
plebbitWebSocketServer.ws.on('connection', (socket, request) => {
console.log('plebbit rpc: new connection')
// debug raw JSON RPC messages in console
if (isDev) {
socket.on('message', (message) => console.log(`plebbit rpc: ${message.toString()}`))
}
})
}
catch (e) {
console.log('failed starting plebbit rpc server', e)
}
pendingStart = false
}
// retry starting the plebbit rpc server every 1 second,
// in case it was started by another client that shut down and shut down the server with it
start()
setInterval(() => {
start()
}, 1000)

View File

@@ -4,7 +4,7 @@
"private": true,
"dependencies": {
"@floating-ui/react": "0.26.1",
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#7aca3428098041eaea2a82ee4b041bb552a4ba99",
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#4d667f7ad7a77cfd416184acffd6f561761ebc9b",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "13.0.0",
"@testing-library/user-event": "13.2.1",
@@ -27,6 +27,9 @@
"react-scripts": "5.0.1",
"react-virtuoso": "4.6.0",
"typescript": "5.2.2",
"tcp-port-used": "1.0.2",
"electron-context-menu": "3.3.0",
"electron-is-dev": "2.0.0",
"zustand": "4.4.3"
},
"scripts": {
@@ -35,6 +38,16 @@
"build-netlify": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true REACT_APP_COMMIT_REF=$COMMIT_REF react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"electron": "yarn electron:before && electron .",
"electron:no-delete-data": "yarn electron:before:download-ipfs && electron .",
"electron:start": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron\"",
"electron:start:no-delete-data": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron:no-delete-data\"",
"electron:build:linux": "electron-builder build --publish never -l",
"electron:build:windows": "electron-builder build --publish never -w",
"electron:build:mac": "electron-builder build --publish never -m",
"electron:before": "yarn electron:before:download-ipfs && yarn electron:before:delete-data",
"electron:before:download-ipfs": "node electron/download-ipfs",
"electron:before:delete-data": "rimraf .plebbit",
"prettier": "prettier src/**/*.{js,ts,tsx} --write",
"changelog": "conventional-changelog --preset angular --infile CHANGELOG.md --same-file --release-count 0"
},
@@ -61,6 +74,49 @@
"devDependencies": {
"@types/memoizee": "0.4.9",
"conventional-changelog-cli": "4.1.0",
"cz-conventional-changelog": "3.3.0"
"concurrently": "8.0.1",
"cross-env": "7.0.3",
"decompress": "4.2.1",
"electron": "19.1.8",
"electron-builder": "23.0.9",
"cz-conventional-changelog": "3.3.0",
"wait-on": "7.0.1"
},
"main": "electron/main.js",
"build": {
"appId": "seedit.desktop",
"productName": "seedit",
"beforePack": "electron/before-pack.js",
"afterAllArtifactBuild": "electron/after-all-artifact-build.js",
"extraResources": [
{
"from": "bin/${os}",
"to": "bin",
"filter": [
"**/*"
]
}
],
"files": [
"build/**/*",
"electron/**/*",
"package.json"
],
"extends": null,
"mac": {
"target": "dmg",
"category": "public.app-category.social-networking",
"type": "distribution"
},
"win": {
"target": [
"portable",
"nsis"
]
},
"linux": {
"target": "AppImage",
"category": "Network"
}
}
}

1448
yarn.lock
View File

File diff suppressed because it is too large Load Diff