In this assignment you will work in a group of 1, 2, or 3 people (but not more) to refactor your Lab 2 solution into a concurrently executing form, and then extend it with additional features including networked interactions to obtain files over sockets from a common server endpoint. You will refactor your code from the previous lab assignment to (1) develop a client program that can conduct high-latency IO operations in parallel using multiple threads according to the fork-join concurrency model, (2) develop a multi-threaded server to support performance of multiple scripts in multiple client programs, and (3) expand the client program's IO model to use networked communication. It is again possible that your programs may encounter lines that are badly formed, and if so should simply skip that line in many cases (though it may print out a warning message saying it did that) and proceed to subsequent steps.
Throughout this assignment, you will again work with input files in particular formats, which the programs you are writing will use as described below. For example, two client programs may be given two different script files, partial_hamlet_act_ii_script.txt and partial_macbeth_act_i_script.txt for the first parts of Act II from William Shakespeare's "Hamlet, Prince of Denmark" and Act I from William Shakespeare's "Macbeth" respectively (text obtained from the Literature Page web site at http://www.literaturepage.com/read/shakespeare_hamlet.html), with corresponding configuration files hamlet_ii_1a_config.txt, hamlet_ii_1b_config.txt, and hamlet_ii_2a_config.txt for the first script file and macbeth_i_1_config.txt, macbeth_i_2a_config.txt, and macbeth_i_2b_config.txt for the second.
As in the previous lab assignments, each scene fragment's configuration file gives the name of each character in the scene fragment along with their corresponding part file, e.g., Polonius_hamlet_ii_1a.txt and Reynaldo_hamlet_ii_1a.txt in hamlet_ii_1a_config.txt, Polonius_hamlet_ii_1b.txt and Ophelia_hamlet_ii_1b.txt in hamlet_ii_1b_config.txt, King_hamlet_ii_2a.txt and Queen_hamlet_ii_2a.txt and Rosencrantz_hamlet_ii_2a.txt and Guildenstern_hamlet_ii_2a.txt in hamlet_ii_2a_config.txt, FIRST_WITCH_macbeth_i_1.txt and SECOND_WITCH_macbeth_i_1.txt and THIRD_WITCH_macbeth_i_1.txt and ALL_macbeth_i_1.txt in macbeth_i_1_config.txt, MALCOLM_macbeth_i_2a.txt and DUNCAN_macbeth_i_2a.txt and SOLDIER_macbeth_i_2a.txt in macbeth_i_2a_config.txt, and LENNOX_macbeth_i_2b.txt and MALCOLM_macbeth_i_2b.txt and DUNCAN_macbeth_i_2b.txt and ROSS_macbeth_i_2b.txt in macbeth_i_2b_config.txt.
rust-2023
) this semester, and must
run correctly on those machines. You are free to use other platforms and
compilers to develop your code, but before submitting your solution you
should please make sure it also compiles without warnings or errors and
runs correctly on the Linux Lab machines, which is where your solution will
be graded.
cargo new lab3client
and
in that package's src
directory (1) create a
lab3
directory and and then (2) copy over the .rs
files
from your Lab 2 solution (with the files from
the previous assignment's lab2
directory going into this assignment's
lab3
directory). Update the code (i.e., replacing lab2
with lab3
in the appropriate comment and code lines) for this new
lab's directory structure.
Compile and run your program and confirm that it still behaves as it did at the end of the previous lab assignment.
println!
macro with uses of writeln!
macro with its first
argument being std::io::stdout().lock()
, and process the
Result
returned by that macro to determine success or failure.
Similarly, replace all uses of the eprintln!
macro with uses of
writeln!
macro with its first argument being
std::io::stderr().lock()
, and process the Result
returned
by that macro to determine success or failure.
Compile and run your program and confirm that it still behaves as it did at the end of the previous lab assignment.
Play
struct so that its vector holds elements of type
Arc<Mutex<SceneFragment>>
instead of
simply SceneFragment
. In your Play
struct's
process_config
method, instead of pushing just a new
SceneFragment
, push a new Arc
initialized with a new
Mutex
initialized with a new SceneFragment
into the vector.
In your Play
struct's prepare
and recite
methods, match on the result of a call to lock
on the appropriate
element of that vector, using a ref
or mut
pattern to
extract an immutable or mutable SceneFragment
reference and invoke
methods using that reference (and for the calls to the enter
and exit
methods additional references obtained in a similar way).
Compile and run your program and confirm that it still behaves as it did at the end of the previous lab assignment when given well formed input files.
SceneFragment
struct so that its vector holds
elements of type Arc<Mutex<Player>>
instead of
simply Player
. In your SceneFragment
struct's
process_config
method, instead of pushing just a new
Player
, push a new Arc
initialized with a new
Mutex
initialized with a new Player
into the vector.
Add a function that compares two references to
Arc<Mutex<Player>>
and has a return type of
std::cmp::Ordering
. The function should match on
calls to the lock
method on each of its passed references, and should
return std::cmp::Ordering::Equal
if either of those returns an error.
Otherwise it should match both results using ref
patterns to obtain
immutable references to the underlying Player
structs, and match on the
result of passing them into a call to Player::partial_cmp
. If that call
returns Some
the function should return the value carried by that enum
label, and otherwise it should return std::cmp::Ordering::Equal
.
Use the function in a call to sort_by
to sort the vector (instead of
simply calling sort
as was done in the
previous lab assignment).
Update your SceneFragment
struct's methods as needed, to match on the
result of a call to lock
on the appropriate element of that vector,
using a ref
or mut
pattern to extract and use an immutable
or mutable SceneFragment
reference instead of calling methods directly
on the vector's elements.
Compile and run your program and confirm that it still behaves as it did at the end of the previous lab assignment when given well formed input files.
Play
struct's process_config
method so that
instead of calling each SceneFragment
struct's prepare
method directly with each configuration file name, it spawns a thread that makes
that call and stores the thread's handle in a collection.
After spawning all those threads the Play
struct's
process_config
method should match on a join with each of their
handles, and should handle any error that is returned from the join as though it
had been returned by calling the method directly.
Modify the SceneFragment
struct's prepare
method so
that if it would have returned an error it instead calls the panic!
macro so that the thread panics and an error result is returned when the thread that
spawned it joins with it.
Compile and run your program and confirm that it still behaves as it did at the end of the previous lab assignment when given well formed input files. Then test your program with a modified script file that will induce file IO errors (e.g., one containing names of config files that do not exist) and make sure those errors are detected and handled by the program as they were in the previous lab assignment.
SceneFragment
struct's
process_config
method so that instead of calling the Player
struct's prepare
method directly with each part file name, it spawns a
thread that makes that call and stores the thread's handle in a collection.
After spawning all those threads the method should join with each of
their handles, and should unwrap the result of each of those joins so that
if the thread being joined had a panic the current thread then also panics (thus
propagating the panic upward).
Modify the Player
struct's prepare
method so that if it
would return an error it instead calls the panic!
macro so that the
thread panics which returns an error result when the thread that spawned it joins with
it.
Compile and run your program and confirm that it still behaves as it did at the end of the previous lab assignment when given well formed input files. Then test your program with a modified script file that will induce file IO errors (e.g., with its config files containing names of player part files that do not exist) and make sure those errors are detected and handled by the program overall as they were in the previous lab assignment.
Create another new Rust package for this assignment: e.g.,
cargo new lab3server
and in that package's src
directory
create a lab3
directory containing empty mod.rs
, and
server.rs
files. Also copy over the return_wrapper.rs
file from your lab 3 client package's lab3
directory into this one.
Modify the main.rs
file so that it declares a public module named
lab3. Modify the mod.rs
file in the lab3
directory so that it declares public modules named server
and
return_wrapper
.
In the server.rs
file declare a Server
struct with a
listener
member of type Option<TcpListener>
and a
listening_addr
member of type String
.
Above that, declare a static
CANCEL_FLAG
variable of
type AtomicBool
that is initialized to false
.
Implement an asssociated new
function for the Server
struct, which initializes the Option
field to be None
and the String
field to be empty.
Implement a is_open
method for the Server
struct, which returns false
if its Option
field is
None
, and otherwise returns true
.
Implement an open
method for the Server
struct, which takes a string slice and calls TcpListener::bind
with it. If that call is successful, the open
method should store
the listener that was returned by it in a Some
label in the
Server
struct's Option
field, and store a copy of the
string slice in the struct's other field (generally speaking this second field
is useful for error and debugging messages but isn't essential to the server's
operation).
Implement a run
method for the Server
struct, which
stays in a loop
as long as the static
CANCEL_FLAG
variable is false
and the
Server
struct's Option
field is not None
.
In each iteration of the loop
, the run
method should
call the accept
method of the listener stored in the struct's
Option
field, and then immediately should again check the
static
CANCEL_FLAG
variable and if it is true
should
return; otherwise, if the accept
call was successful the
run
method should spawn a child thread (e.g., using a
move
closure) to manage the newly accepted socket connection.
The child thread should read in a text token from the accepted socket connection,
and if the token is "quit"
the thread should store
the
value true
in the CANCEL_FLAG
variable and return. Otherwise, the thread should
treat the token as the name of a file and try to open that file for reading. If
the file cannot be opened, the thread should shut down the connection and return.
Otherwise, the thread should read in all the contents of the file and write them
out over the connection and then return. For security purposes, your server
may assume that the only files it should open and stream out over the connection
will reside within the current directory in which the server is running, which
will be true in all of the test cases I will run in evaluating your lab solution.
That is, the server can check the token for any characters indicating a directory
path or expansion of an environment variable (including /
or
\
or ..
or $
) and decline to try to open
the file if any of those are found.
Modify the main
function in the main.rs
file so that it
has a return type of ReturnWrapper
. It should check that the command
line arguments to the program have exactly two tokens, one with the program's name
and one with a network address at which the server will listen and accept
connections, and if not should print a usage message and return an error code.
Otherwise, the main
function should declare a variable initialized
with Server::new()
, pass the network address token into a call to
its open
method, and then call its run
method.
Create another new Rust package for a simple test client with which to validate
the server's behavior: e.g., cargo new lab3testclient
and in that
package's src
directory modify the main
function in the
main.rs
file so that it checks that the command
line arguments to the program have exactly three tokens, one with the program's
name, one with a network address with which to connect to a server, and one with
a token to send to the server. If the wrong number of command line arguments
was given, the main
function should print a usage message and return
an error code.
Otherwise, the main
function should pass a copy of the network
address into a call to TcpStream::connect
, and if a connection was
successfully established should send the token to the server over that connection.
If the token was anything other than "quit"
, the test client should
read lines of text from the connection and print each one out, until the
connection is shut down by the server at which point the test client's
main
function should return Ok(())
.
If the token was "quit"
, the test client should instead declare a
variable of type std::time::Duration
that is initialized to a value
of one second, pass that variable into a call to std::thread::sleep
,
call TcpStream::connect
again with the same network address (to wake
up the server out of the accept
call) and then return
Ok(())
.
Add a get_buffered_reader
function to the script_gen.rs
file in your lab3client
package, which takes an immutable reference
to a String
and returns a Result
with a newly initialized
BufReader
.
The get_buffered_reader
function should check whether the string
that was passed to it begins with "net:"
followed by an 8-digit dotted
decimal address and another colon, followed by a port number and another colon,
followed by a file name (e.g.,
"net:127.0.0.1:7777:partial_macbeth_act_i_script.txt"
). If it does,
the get_buffered_reader
function should separate out a token
containing just its dotted decimal address and port number (with a colon between
them) and pass that token into a call to TcpStream::connect
.
If that call fails, the get_buffered_reader
function should return
an error, or otherwise if it succeeds should (1) separate out a token with the
file name that follows the colon after the port number, and send it
to the server over the connection, and then (2) use the connection handle to
initialize and return a new BufReader
.
If the string passed into the get_buffered_reader
function is not
formatted in that way, the function should use it as a file name, call
File::open
with it, and use the file handle to initialize and return
a new BufReader
(or should return an error if one occurred during
any of those steps).
Modify the grab_trimmed_file_lines
function in the
script_gen.rs
file so that instead of opening a file and wrapping
the file handle in a BufReader
, it instead calls the
get_buffered_reader
function and checks the result of that call.
lab3client
program and by modifying some entries in the script
files and configuration files so that they specify remote files as well (e.g.,
"net:127.0.0.1:7777:hamlet_ii_1a_config.txt"
or
"net:127.0.0.1:7777:DUNCAN_macbeth_i_2a.txt"
).
lab3client
program at once, some with the same script file and
some with different script files, and involving both local and remote files.
ReadMe.txt
file please add a subsection titled "Testing"
and in it please summarize how you tested your solution, including any problems your
testing detected, and how you addressed those.
The first section of your ReadMe.txt file should include:
The second section of your ReadMe.txt file should provide detailed instructions for how to:
rustc
and cargo
and other tools already present on the CEC
Linux Lab machines.The third section of your ReadMe.txt file should provide a reasonably detailed description of how you developed and tested your solution, including each stage of how you refactored and extended your Lab 2 solution to implement this lab assignment. Please also describe the kinds of script, configuration, and character part files you used and their formats (including well formed and badly formed content and local and remote file names to test how your program handled those variations), and any other scenarios that you tested that you consider important.
lab3client
, lab3server
, and lab3testclient
packages;