Merge pull request #327 from GNS3/enhanced-configuration

Enhanced configuration of local server; Binary dependencies
This commit is contained in:
ziajka 2019-03-05 12:42:04 +01:00 committed by GitHub
commit 2b0bb19f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 201 additions and 34 deletions

View File

@ -23,6 +23,7 @@ build_script:
- "%PYTHON%\\python.exe scripts\\build.py download" - "%PYTHON%\\python.exe scripts\\build.py download"
- "%PYTHON%\\python.exe scripts\\build.py build_exe -b dist/exe.gns3server -s" - "%PYTHON%\\python.exe scripts\\build.py build_exe -b dist/exe.gns3server -s"
- "%PYTHON%\\python.exe scripts\\build.py validate -b dist" - "%PYTHON%\\python.exe scripts\\build.py validate -b dist"
- "%PYTHON%\\python.exe scripts\\build.py download_dependencies -b dist"
- yarn electron-builder --win --x64 - yarn electron-builder --win --x64
test: off test: off

View File

@ -19,6 +19,9 @@ files:
extraFiles: extraFiles:
- dist/exe.gns3server/** - dist/exe.gns3server/**
- dist/ubridge/**
- dist/vpcs/**
- dist/dynamips/**
mac: mac:
category: public.app-category.developer-tools category: public.app-category.developer-tools

View File

@ -2,6 +2,7 @@ const { spawn } = require('child_process');
const kill = require('tree-kill'); const kill = require('tree-kill');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const ini = require('ini');
const { ipcMain } = require('electron') const { ipcMain } = require('electron')
const { app } = require('electron') const { app } = require('electron')
@ -10,19 +11,13 @@ const isWin = /^win/.test(process.platform);
let runningServers = {}; let runningServers = {};
exports.getLocalServerPath = async () => { exports.getLocalServerPath = async () => {
const lookupDirectories = [ let binary = isWin ? 'gns3server.exe': 'gns3server';
__dirname, return findBinary('exe.', binary);
path.dirname(app.getPath('exe')) }
];
for(var directory of lookupDirectories) { exports.getUbridgePath = async () => {
const serverPath = await findLocalServerPath(directory); let binary = isWin ? 'ubridge.exe': 'ubridge';
if(serverPath !== undefined) { return findBinary('ubridge', binary);
return serverPath;
}
}
return;
} }
exports.startLocalServer = async (server) => { exports.startLocalServer = async (server) => {
@ -43,7 +38,21 @@ exports.stopAllLocalServers = async () => {
return await stopAll(); return await stopAll();
} }
async function findLocalServerPath(baseDirectory) { async function findBinary(binaryDirectory, filename) {
const lookupDirectories = [
__dirname,
path.dirname(app.getPath('exe'))
];
for(var directory of lookupDirectories) {
const serverPath = await findBinaryInDirectory(directory, binaryDirectory, filename);
if(serverPath !== undefined) {
return serverPath;
}
}
}
async function findBinaryInDirectory(baseDirectory, binaryDirectory, filename) {
const distDirectory = path.join(baseDirectory, 'dist'); const distDirectory = path.join(baseDirectory, 'dist');
if (!fs.existsSync(distDirectory)) { if (!fs.existsSync(distDirectory)) {
@ -52,27 +61,23 @@ async function findLocalServerPath(baseDirectory) {
const files = fs.readdirSync(distDirectory); const files = fs.readdirSync(distDirectory);
let serverPath = null; let binaryPath = null;
files.forEach((directory) => { files.forEach((directory) => {
if(directory.startsWith('exe.')) { if(directory.startsWith(binaryDirectory)) {
if (isWin) { binaryPath = path.join(baseDirectory, 'dist', directory, filename);
serverPath = path.join(baseDirectory, 'dist', directory, 'gns3server.exe');
}
else {
serverPath = path.join(baseDirectory, 'dist', directory, 'gns3server');
}
} }
}); });
if(serverPath !== null && fs.existsSync(serverPath)) { if(binaryPath !== null && fs.existsSync(binaryPath)) {
return serverPath; return binaryPath;
} }
return; return;
} }
function getServerArguments(server, overrides) {
function getServerArguments(server, overrides, configPath) {
let serverArguments = []; let serverArguments = [];
if(server.host) { if(server.host) {
serverArguments.push('--host'); serverArguments.push('--host');
@ -82,6 +87,14 @@ function getServerArguments(server, overrides) {
serverArguments.push('--port'); serverArguments.push('--port');
serverArguments.push(server.port); serverArguments.push(server.port);
} }
serverArguments.push('--local');
if(configPath) {
serverArguments.push('--config');
serverArguments.push(configPath);
}
return serverArguments; return serverArguments;
} }
@ -153,6 +166,56 @@ async function stop(serverName) {
return stopped; return stopped;
} }
async function getIniFile(server) {
return path.join(app.getPath('userData'), `gns3_server_${server.id}.ini`);
}
async function configure(configPath, server) {
if(!fs.existsSync(configPath)) {
fs.closeSync(fs.openSync(configPath, 'w'));
console.log(`Configuration file '${configPath}' has been created.`);
}
var config = ini.parse(fs.readFileSync(configPath, 'utf-8'));
if(server.path) {
config.path = server.path;
}
if(server.host) {
config.host = server.host;
}
if(server.port) {
config.port = server.port;
}
if(server.ubridge_path) {
config.ubridge_path = server.ubridge_path;
}
fs.writeFileSync(configPath, ini.stringify(config, { section: 'Server' }));
}
async function setPATHEnv() {
const vpcsLookup = [
path.join(__dirname, 'dist', 'vpcs'),
path.join(path.dirname(app.getPath('exe')), 'dist', 'vpcs')
];
const dynamipsLookup = [
path.join(__dirname, 'dist', 'dynamips'),
path.join(path.dirname(app.getPath('exe')), 'dist', 'dynamips')
];
// prevent adding duplicates
let extra = [
...vpcsLookup,
...dynamipsLookup
].filter((dir) => {
return process.env.PATH.indexOf(dir) < 0;
});
extra.push(process.env.PATH);
process.env.PATH = extra.join(";");
}
async function run(server, options) { async function run(server, options) {
if(!options) { if(!options) {
options = {}; options = {};
@ -161,9 +224,17 @@ async function run(server, options) {
const logStdout = options.logStdout || false; const logStdout = options.logStdout || false;
const logSterr = options.logSterr || false; const logSterr = options.logSterr || false;
console.log(`Configuring`);
const configPath = await getIniFile(server);
await configure(configPath, server);
console.log(`Setting up PATH`);
await setPATHEnv();
console.log(`Running '${server.path}'`); console.log(`Running '${server.path}'`);
let serverProcess = spawn(server.path, getServerArguments(server)); let serverProcess = spawn(server.path, getServerArguments(server, {}, configPath));
notifyStatus({ notifyStatus({
serverName: server.name, serverName: server.name,
@ -225,14 +296,15 @@ async function main() {
}); });
} }
ipcMain.on('local-server-run', async function (event, server) { if(ipcMain) {
const responseChannel = getChannelForServer(); ipcMain.on('local-server-run', async function (event, server) {
await run(server); const responseChannel = getChannelForServer();
event.sender.send(responseChannel, { await run(server);
success: true event.sender.send(responseChannel, {
success: true
});
}); });
}); }
if (require.main === module) { if (require.main === module) {
process.on('SIGINT', function() { process.on('SIGINT', function() {

View File

@ -58,6 +58,7 @@
"css-tree": "^1.0.0-alpha.29", "css-tree": "^1.0.0-alpha.29",
"d3-ng2-service": "^2.1.0", "d3-ng2-service": "^2.1.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"ini": "^1.3.5",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"ng2-file-upload": "^1.3.0", "ng2-file-upload": "^1.3.0",
"ngx-electron": "^2.1.1", "ngx-electron": "^2.1.1",
@ -66,9 +67,9 @@
"raven-js": "^3.27.0", "raven-js": "^3.27.0",
"rxjs": "^6.4.0", "rxjs": "^6.4.0",
"rxjs-compat": "^6.4.0", "rxjs-compat": "^6.4.0",
"tree-kill": "^1.2.1",
"typeface-roboto": "^0.0.54", "typeface-roboto": "^0.0.54",
"yargs": "^13.2.1", "yargs": "^13.2.1",
"tree-kill": "^1.2.1",
"zone.js": "^0.8.29" "zone.js": "^0.8.29"
}, },
"devDependencies": { "devDependencies": {

View File

@ -38,6 +38,39 @@ WORKING_DIR = os.path.join(FILE_DIR, 'tmp')
SOURCE_ZIP = os.path.join(WORKING_DIR, 'gns3-server.source.zip') SOURCE_ZIP = os.path.join(WORKING_DIR, 'gns3-server.source.zip')
SOURCE_DESTINATION = os.path.join(WORKING_DIR, 'source') SOURCE_DESTINATION = os.path.join(WORKING_DIR, 'source')
BINARIES_EXTENSION = platform.system() == "Windows" and ".exe" or "" BINARIES_EXTENSION = platform.system() == "Windows" and ".exe" or ""
DEPENDENCIES = {
'ubridge': {
'releases': 'https://api.github.com/repos/GNS3/ubridge/releases',
'version': 'LATEST',
'files': {
'windows': [
'cygwin1.dll',
'ubridge.exe'
]
}
},
'vpcs': {
'releases': 'https://api.github.com/repos/GNS3/vpcs/releases',
'version': '0.6.1',
'files': {
'windows': [
'cygwin1.dll',
'vpcs.exe'
]
}
},
'dynamips': {
'releases': 'https://api.github.com/repos/GNS3/dynamips/releases',
'version': '0.2.17',
'files': {
'windows': [
'cygwin1.dll',
'dynamips.exe',
'nvram_export.exe'
]
}
}
}
def download(url, output): def download(url, output):
@ -80,6 +113,34 @@ def prepare():
os.makedirs(WORKING_DIR, exist_ok=True) os.makedirs(WORKING_DIR, exist_ok=True)
def download_dependencies_command(arguments):
output_directory = os.path.join(os.getcwd(), arguments.b)
for name, definition in DEPENDENCIES.items():
response = requests.get(definition['releases'])
response.raise_for_status()
releases = response.json()
if definition['version'] == 'LATEST':
release = releases[0]
else:
release = list(filter(lambda x: x['tag_name'] == "v{}".format(definition['version']), releases))[0]
dependency_dir = os.path.join(output_directory, name)
os.makedirs(dependency_dir, exist_ok=True)
files = []
if platform.system() == "Windows":
files = definition['files']['windows']
for filename in files:
dependency_file = os.path.join(dependency_dir, filename)
dependency_url = list(filter(lambda x: x['name'] == filename, release['assets']))[0]['browser_download_url']
download(dependency_url, dependency_file)
print('Downloaded {} to {}'.format(filename, dependency_file))
def download_command(arguments): def download_command(arguments):
shutil.rmtree(SOURCE_DESTINATION, ignore_errors=True) shutil.rmtree(SOURCE_DESTINATION, ignore_errors=True)
os.makedirs(SOURCE_DESTINATION) os.makedirs(SOURCE_DESTINATION)
@ -243,6 +304,9 @@ if __name__ == '__main__':
parser_validate = subparsers.add_parser('validate', help='Validate build') parser_validate = subparsers.add_parser('validate', help='Validate build')
parser_validate.add_argument('-b', help='Output directory') parser_validate.add_argument('-b', help='Output directory')
parser_validate = subparsers.add_parser('download_dependencies', help='Download dependencies')
parser_validate.add_argument('-b', help='Output directory')
args = parser.parse_args() args = parser.parse_args()
if args.command == 'build_exe': if args.command == 'build_exe':
@ -251,6 +315,9 @@ if __name__ == '__main__':
elif args.command == 'download': elif args.command == 'download':
prepare() prepare()
download_command(args) download_command(args)
elif args.command == 'download_dependencies':
prepare()
download_dependencies_command(args)
elif args.command == 'validate': elif args.command == 'validate':
prepare() prepare()
validate_command(args) validate_command(args)

View File

@ -16,6 +16,10 @@
<input matInput tabindex="1" formControlName="path" placeholder="Local server path" /> <input matInput tabindex="1" formControlName="path" placeholder="Local server path" />
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="serverForm.get('location').value === 'local'">
<input matInput tabindex="1" formControlName="ubridge_path" placeholder="Ubridge path" />
</mat-form-field>
<mat-form-field> <mat-form-field>
<input matInput tabindex="1" formControlName="host" placeholder="Host" /> <input matInput tabindex="1" formControlName="host" placeholder="Host" />
</mat-form-field> </mat-form-field>

View File

@ -18,6 +18,7 @@ export class AddServerDialogComponent implements OnInit {
'name': new FormControl('', [ Validators.required ]), 'name': new FormControl('', [ Validators.required ]),
'location': new FormControl(''), 'location': new FormControl(''),
'path': new FormControl(''), 'path': new FormControl(''),
'ubridge_path': new FormControl(''),
'host': new FormControl('', [ Validators.required ]), 'host': new FormControl('', [ Validators.required ]),
'port': new FormControl('', [ Validators.required, Validators.min(1) ]), 'port': new FormControl('', [ Validators.required, Validators.min(1) ]),
'authorization': new FormControl('none'), 'authorization': new FormControl('none'),
@ -72,24 +73,39 @@ export class AddServerDialogComponent implements OnInit {
return; return;
} }
async getDefaultUbridgePath() {
if(this.electronService.isElectronApp) {
return await this.electronService.remote.require('./local-server.js').getUbridgePath();
}
return;
}
async ngOnInit() { async ngOnInit() {
this.locations = await this.getLocations(); this.locations = await this.getLocations();
const defaultLocalServerPath = await this.getDefaultLocalServerPath(); const defaultLocalServerPath = await this.getDefaultLocalServerPath();
const defaultUbridgePath = await this.getDefaultUbridgePath();
this.serverForm.get('location').valueChanges.subscribe((location: string) => { this.serverForm.get('location').valueChanges.subscribe((location: string) => {
const pathControl = this.serverForm.get('path'); const pathControl = this.serverForm.get('path');
const ubridgePathControl = this.serverForm.get('ubridge_path');
if(location === 'local') { if(location === 'local') {
pathControl.setValue(defaultLocalServerPath); pathControl.setValue(defaultLocalServerPath);
pathControl.setValidators([Validators.required]); pathControl.setValidators([Validators.required]);
ubridgePathControl.setValue(defaultUbridgePath);
ubridgePathControl.setValidators([Validators.required]);
} }
else { else {
pathControl.setValue(''); pathControl.setValue('');
pathControl.clearValidators(); pathControl.clearValidators();
ubridgePathControl.setValue('');
ubridgePathControl.clearValidators();
} }
[pathControl].forEach((control) => { [pathControl, ubridgePathControl].forEach((control) => {
control.updateValueAndValidity({ control.updateValueAndValidity({
onlySelf: true onlySelf: true
}); });

View File

@ -38,9 +38,11 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy {
// attach to notification stream when any of running local servers experienced issues // attach to notification stream when any of running local servers experienced issues
this.serverStatusSubscription = this.serverManagement.serverStatusChanged.subscribe((serverStatus) => { this.serverStatusSubscription = this.serverManagement.serverStatusChanged.subscribe((serverStatus) => {
if(serverStatus.status === 'errored') { if(serverStatus.status === 'errored') {
console.error(serverStatus.message);
this.toasterService.error(serverStatus.message); this.toasterService.error(serverStatus.message);
} }
if(serverStatus.status === 'stderr') { if(serverStatus.status === 'stderr') {
console.error(serverStatus.message);
this.toasterService.error(serverStatus.message); this.toasterService.error(serverStatus.message);
} }
}); });

View File

@ -9,6 +9,7 @@ export class Server {
host: string; host: string;
port: number; port: number;
path: string; path: string;
ubridge_path: string;
authorization: ServerAuthorization; authorization: ServerAuthorization;
login: string; login: string;
password: string; password: string;