2022-04-25 12:24:44 -05:00

220 lines
7.5 KiB
Ruby

module VagrantPlugins
module CommandServe
class Mappers
module Internal
class Graph
# Provides searching of a graph to determine if a
# destination vertex is accessible from a given
# root vertex.
class Search
class NoPathError < StandardError; end
# Value used for marking visited vertices
VERTEX_ID = :vertex
attr_reader :graph, :root, :visited
# Create a new DFS instance
#
# @param graph [Graph] Graph used for searching
def initialize(graph:)
@graph = graph.copy
@m = Mutex.new
@root = nil
@visited = nil
end
# Provide a list of vertices not visited to build
# the generated path. If no path has been generated,
# it will return an empty value by default.
#
# @return [Array<Vertex>] list of vertices
def orphans
@m.synchronize do
load_orphans
end
end
# Generate a path from the given source vertex to
# the given destination vertex using a depth first
# search algorithm.
#
# @param src [Vertex] Source vertex
# @param dst [Vertex] Destination vertex
# @return [Array<Vertex>] path from source to destination
# @raises [NoPathError] when no path can be determined
def path(src, dst)
@m.synchronize do
@root = src
@visited = {}
stack = Stack.new
stack.push(src)
p = find_path(src, dst, stack)
if Array(p).empty?
@visited = nil
raise NoPathError,
"failed to determine valid path"
end
# Now ensure our final path is in the correct order
# based on edge defined dependencies
load_orphans.each do |v|
graph.remove(v)
end
t = Topological.new(graph: graph)
t.sort
end
end
protected
# Find path from source to destination
#
# @param src [Vertex] Source vertex
# @param dst [Vertex] Destination vertex
# @param s [Stack] Stack for holding path information
# @return [Array<Vertex>, nil] list of vertices or nil if not found
def find_path(src, dst, s)
# If we have reached our destination then it's
# time to mark vertices included in the final
# path as visited and return the path
if src == dst
p = s.values
p.each do |v|
visited[v.hash_code] = VERTEX_ID
end
return p
end
graph.edges_out(src).each do |v|
# If the incoming edges don't define dependencies
# then we only care about a single path through
# the vertex
if !v.incoming_edges_required
if s.include?(v)
next
end
s.push(v)
if p = find_path(v, dst, s)
return p
end
s.pop
next
end
# Since the incoming edges define dependencies
# we need to validate they are reachable from
# root before we continue attempting to build
# the path
req_paths = []
if graph.edges_out(v).size > 1
d = self.class.new(graph: graph.reverse)
begin
p = d.path(dst, v)
req_paths << p
rescue NoPathError
next
end
end
failed = false
graph.edges_in(v).each do |in_v|
# This was our initial path here so ignore
if src.hash_code == in_v.hash_code
next
end
# Create a new graph but reverse the direction
new_g = graph.reverse
# Remove edges from the vertex except for the
# one we are currently processing
new_g.edges_out(v).each do |out_v|
if out_v.hash_code == in_v.hash_code
next
end
new_g.remove_edge(v, out_v)
end
# Create a new DFS to search using the reversed
# graph
new_d = self.class.new(graph: new_g)
begin
# Attempt to find a path from the vertex back
# to root
p = new_d.path(v, root)
req_paths << p
rescue NoPathError
# If a path could not be found, mark failed
# and bail
failed = true
break
end
end
# If all the incoming edges couldn't be satisfied
# then we ignore this vertex and move on
if failed
next
end
# All the incoming edges could be satisfied so
# we push this vertex on the stack and continue
# down the current path
s.push(v)
# If we were able to reach the destination on
# this path, we now need to add in the extra
# paths generated to satisfy the edge requirements
if p = find_path(v, dst, s)
# Store the position of the vertex we are processing
# in the path so we can properly cut and glue the path
pos = p.index(v)
# Cut the path prior to our current vertex
path_start = p.slice(0, pos)
# Now walk through each extra path and add any vertices
# that have not yet been seen
req_paths.each do |extra_path|
# The extra path originated from a reversed graph, so
# reverse this path before processing
extra_path.reverse.each do |extra_v|
if visited.key?(extra_v.hash_code)
next
end
visited[extra_v.hash_code] = VERTEX_ID
path_start << extra_v
end
end
# Now glue the remaining path to our modified start
# path and return the result
return path_start + p.slice(pos, p.size)
end
# If we made it here the vertex isn't part of a valid
# path so pop it off the stack and continue
s.pop
end
# If we have nothing left to process, return nothing
nil
end
def load_orphans
if visited.nil?
return []
end
graph.vertices.map do |v|
v if !visited.key?(v.hash_code)
end.compact
end
end
end
end
end
end
end