2.2.3 Cycle 3 - Configuration Handler

Design

Objectives

For this cycle the main objective is to turn the node software into something more aligned with what a user would actually be able to use, as although it will mainly be computer oriented people using this part of the project having it easy to understand is still a big benefit.

Usability Features

Configuration handler

For the configuration handler it will originally start of very simply and will probably be somewhat a pain to use, but the hope is that by laying out a ground framework now that contains the console interface that will be used to interact with the configuration handler to be in plain, easy to understand English. This will give users the best chance at being able to run through the configuration setup with as little friction as possible.

  • Plain, easy to read English text based interface.

  • Allows user to respond in a variety of ways ("yes", "y","confirm","approve", etc).

Key Variables

Variable Name
Use

user_config

to store the user's current configuration settings for various parts of the system. (which port to use for the server, etc)

config_path

the default place for user configurations to be stored by the software for loading/saving configs.

Pseudocode

The handler will be built with three main functions:

  • get_config meant for collecting the configuration for use by other parts of the program.

  • create_configuration meant for generating a new configuration for the user if they don't already have one.

  • save_config meant for saving the current version of the configuration to the file.

Getting the configuration.

get_config will allow any part of the software to load the newest version of the configuration, ideally this will be from memory but will likely just be from the file to start with. It will look something like the following:

// Psuedocode - get configuration function

CONST config_path = "./monochain/node.config" // the key variable described earlier.

FUNCTION get_config():
 	// load the config from the defualt file location and decode with json.
	user_config = file.read(config_path, "json")

	// check if the config loaded properly
	IF user_config DOES NOT EXIST:
		// if it hasn't, let the user know and start generating a new one.
		OUTPUT "No config detected, or error occoured."
		user_config = new_config()
	END IF
	
	// if generating a new config failed aswell, then exit with code 100
	IF !user_config.loaded 
		OUTPUT "Failed to create a new configuration file.\nExiting..."
		exit(100)
	ENDIF

	// if we make it to this part of the code then the config is ok
	OUTPUT "\nConfig Loaded..."
	return user_config
END FUNCTION

Generating a new config

create_configuration is a pretty simple function that collects data from other functions - such as the ask_for_port one also in the pseudocode below - and formats that data into a configuration object before saving it and returning the config to wherever it has been called from.

// Psuedocode - create a new configuration

FUNCTION create_configuration():
	// create the config object
	config = {
		// let's the program know the config loaded successfully
		loaded: true	
		// generate the port to use in the function below	
		port: ask_for_port()
	}
	
	// save the config object to a file
	save_config(config)
	return config
END FUNCTION


FUNCTION ask_for_port():
	// ask the user for the port 
	OUTPUT "What port would you like to run your node on?"
	port = INPUT

	// check the port is valid
	IF port > 65535 || port < 1:
		// if it's not valid, tell the user and ask for the port again
		OUTPUT "That port does not exist! You might want to enter a number between 1 and 65535."
		return ask_for_port()
	END IF

	// return the port
	return port
END FUNCTION

Saving the configuration

This function simply saves the configuration to a file so that it can be loaded/updated by other parts of the program as needed, and will keep its contents if the node/computer running the program is shut down or stopped.

The code will look something like the following:

// Psuedocode - save the configuration

FUNCTION save_config(config)
	// convert the data into something safe to be saved into a file 
	data = json.encode(config)
	
	// write the encoded data to the disk at the path ./node.config"
	file.write("./node.config", data)
END FUNCTION

Development

Most of the development for this cycle went pretty smoothly, which was very nice!

This was mainly due to the module being relatively simple, so most of the development was simply converting the pseudocode and concept specified above into V code.

In order to do this I first created the "configuration" module, which is done simply by creating a new folder with the name of the module and adding the line module configuration to the top of any files that are part of that module.

Once the module was setup I created three files for the code to help separate it out and make it easier for anyone looking at the code base to see what was going on, the files were:

  • configHandler.v - This is the main file of the module and contains the get_config() function mentioned earlier, this then calls on a function from the configFileHandler.v file below and if there is a config file setup, loads it, and if not then calls on the configCreator.v file to create a new configuration.

  • configCreator.v - This just houses some basic user interfacing functions that ask for various pieces of data required to generate a new configuration and then turns that into a new configuration object and saves it using the configFileHandler.v file below.

  • configFileHandler.v - This is a basic file used to load, save, and check the existence of configuration files, any file read through this is type safe due to the language being used (VLang) reading json data using the expected object type and rejecting the file if it is incorrectly formatted. This means that all the data loaded to and from files using this part of the module are the correct object types and shouldn't cause any formatting crashes in other parts of the program.

Outcome

Because there is actually quite a lot of code in this module, I will just include some major functions such as the ones mentioned in the pseudocode.

Getting the config file

// Vlang Code - "./packages/node/src/configuration/configHandler.v"

pub fn get_config() UserConfig {
	// the function used below can be found in the configFileHandler file
	// it is also in the final code subsection further down this page
	mut user_config := load_config()

	if !user_config.loaded {
		println("No config detected, or error occoured.")
		// the following function asks the user if they want to generate
		// a new config and if so takes them to the "create_configuration" function.
		user_config = new_config(0)
	}

	if !user_config.loaded {
		// if something went wrong, quit
		eprintln("Failed to create a new configuration file, unexpected value was $user_config ")
		exit(100)
	}
	
	// nothing went wrong! So load the next part of the program.
	println("\nConfig Loaded, launching API server...")
	return user_config
}

This version of the code can be found on Github Commit bc45df98e7.

Generating a new config

// Vlang Code - "./packages/node/src/configuration/configCreator.v"

pub fn create_configuration() UserConfig {
	// build the config object using subfunctions
	config := UserConfig{
		loaded: true
		port: ask_for_port(0)
	}

	// save the config
	save_config(config, 0)
	return config
}

fn ask_for_port(recursion_depth int) int {
	// this just asks the user what port they'd like to run their node on
	// then sends that data back to the config creator.
	mut port := (read_line("What port would you like to run your node on (default: 8001)?\n$:") or { 
		eprintln("Input failed, please try again")
		utils.recursion_check(recursion_depth, 2)
		return ask_for_port(recursion_depth + 1)
	}).int()

	if port > 65535 || port < 1 {
		eprintln("That port does not exist! You might want to enter a number between 1 and 65535.")
		utils.recursion_check(recursion_depth, 3)
		return ask_for_port(recursion_depth + 1)
	}

	return port
}

This version of the code can be found on Github Commit bc45df98e7.

Saving the configuration files

This includes the save_config function as expected, but also the load_config function which was originally planned to just be built into the get_config function from earlier.

// Vlang Code - "./packages/node/src/configuration/configFileHandler.v"

pub fn save_config(config UserConfig, recursion_depth int) bool {
	// setup a failure tracker
	mut failed := false

	// encode the data
	data := json.encode(config)
	
	// this shouldn't be here but it is in the commit referenced so I left it in.
	// it doesn't error, it'll just never get run.
	if failed {return false}

	// save the file
	os.write_file("./node.config", data) or {
		// if this gets run then something went wrong.
		eprintln('Failed to save file, trying again.')
		if recursion_depth >= 5 {
			// if it's already failed multiple times then quit
			eprintln("Failed to save file too many times, continuing with program but your config won't be saved")
			failed = true
		} else {
			// if no recursion issues, then just try again
			failed = !save_config(config, recursion_depth + 1)
		}
	}


	if !failed {
		println("Sucessfully saved configuration file.") 
	}
	return !failed
}

pub fn load_config() UserConfig {
	if os.exists("./node.config") {
		// config file already exists, make sure we can open and decode it

		config_raw := os.read_file("node.config") or {
			// something went wrong opening the file, return null
			eprintln('Failed to open config file, error $err')
			return UserConfig{ loaded: false }
		}

		user_config := json.decode(UserConfig, config_raw) or {
			// something went wrong decoding the file, return null
			eprintln('Failed to decode json, error: $err')
			return UserConfig{ loaded: false }
		}

		return user_config
	}

	return UserConfig{ loaded: false }
} 

This version of the code can be found on Github Commit bc45df98e7.

Testing

Tests

Test
Instructions
What I expect
What actually happens
Pass/Fail

1

Create a new configuration file using "create_configuration()"

Console logs to confirm the operation is commencing and a configuration file to be generated.

As Expected

Pass

2

Load the new config file using "load_config()"

Config file should be loaded and returned

As Expected

Pass

3

Load the new config file using "get_config()"

Config file should be loaded and returned

As Expected

Pass

Evidence

Test 1 Passed
Test 2 Passed
Test 3 Passed

Last updated