VOID_DataLogger: Finish writing and close the file when we're destructed.
[VOID.git] / VOID_DataLogger.cs
blob:a/VOID_DataLogger.cs -> blob:b/VOID_DataLogger.cs
// VOID // VOID
// //
// VOID_DataLogger.cs // VOID_DataLogger.cs
// //
// Copyright © 2014, toadicus // Copyright © 2014, toadicus
// All rights reserved. // All rights reserved.
// //
// Redistribution and use in source and binary forms, with or without modification, // Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met: // are permitted provided that the following conditions are met:
// //
// 1. Redistributions of source code must retain the above copyright notice, // 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer. // this list of conditions and the following disclaimer.
// //
// 2. Redistributions in binary form must reproduce the above copyright notice, // 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation and/or other // this list of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution. // materials provided with the distribution.
// //
// 3. Neither the name of the copyright holder nor the names of its contributors may be used // 3. Neither the name of the copyright holder nor the names of its contributors may be used
// to endorse or promote products derived from this software without specific prior written permission. // to endorse or promote products derived from this software without specific prior written permission.
// //
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   
using KSP; using KSP;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using ToadicusTools; using ToadicusTools;
using UnityEngine; using UnityEngine;
   
namespace VOID namespace VOID
{ {
public class VOID_DataLogger : VOID_WindowModule, IVOID_BehaviorModule public class VOID_DataLogger : VOID_WindowModule, IVOID_BehaviorModule
{ {
/* /*
* Fields * Fields
* */ * */
protected bool stopwatch1_running; #region Fields
   
protected bool _loggingActive; protected bool _loggingActive;
protected bool first_write; protected bool firstWrite;
   
protected double stopwatch1; [AVOID_SaveValue("logInterval")]
  protected VOID_SaveValue<float> logInterval;
protected string csv_log_interval_str; protected string logIntervalStr;
   
protected float csv_log_interval; protected float csvCollectTimer;
   
protected double csvCollectTimer; protected List<byte> csvBytes;
   
protected System.Text.UTF8Encoding utf8Encoding; protected string _fileName;
protected FileStream _outputFile; protected FileStream _outputFile;
   
protected uint outstandingWrites; protected uint outstandingWrites;
   
protected List<string> csvList = new List<string>(); protected System.Text.UTF8Encoding _utf8Encoding;
   
  #endregion
   
/* /*
* Properties * Properties
* */ * */
   
  #region Properties
   
// TODO: Add configurable or incremental file names. // TODO: Add configurable or incremental file names.
protected bool loggingActive protected bool loggingActive
{ {
get get
{ {
return this._loggingActive; return this._loggingActive;
} }
set set
{ {
if (value != this._loggingActive) if (value != this._loggingActive)
{ {
if (value) if (value)
{ {
  this.csvCollectTimer = 0f;
} }
else else
{ {
if (this._outputFile != null) this.CloseFileIfOpen();
{  
Tools.DebugLogger logger = Tools.DebugLogger.New(this);  
   
logger.Append("CSV logging disabled, ");  
   
logger.Append("disposing file.");  
logger.Print();  
this.outputFile.Dispose();  
this._outputFile = null;  
}  
} }
   
this._loggingActive = value; this._loggingActive = value;
} }
} }
} }
   
protected string fileName protected string fileName
{ {
get get
{ {
return KSP.IO.IOUtils.GetFilePathFor( if (this._fileName == null || this._fileName == string.Empty)
typeof(VOID_Core), {
string.Format( this._fileName = KSP.IO.IOUtils.GetFilePathFor(
"{0}_{1}", typeof(VOIDCore),
this.vessel.vesselName, string.Format(
"data.csv" "{0}_{1}",
), this.vessel.vesselName,
null "data.csv"
); ),
  null
  );
  }
   
  return this._fileName;
} }
} }
   
protected FileStream outputFile protected FileStream outputFile
{ {
get get
{ {
if (this._outputFile == null) if (this._outputFile == null)
{ {
Tools.DebugLogger logger = Tools.DebugLogger.New(this); Tools.DebugLogger logger = Tools.DebugLogger.New(this);
logger.AppendFormat("Initializing output file '{0}' with mode ", this.fileName); logger.AppendFormat("Initializing output file '{0}' with mode ", this.fileName);
   
if (File.Exists(this.fileName)) if (File.Exists(this.fileName))
{ {
logger.Append("append"); logger.Append("append");
this._outputFile = new FileStream(this.fileName, FileMode.Append, FileAccess.Write, FileShare.Write, 512, true); this._outputFile = new FileStream(
  this.fileName,
  FileMode.Append,
  FileAccess.Write,
  FileShare.Read,
  512,
  true
  );
} }
else else
{ {
logger.Append("create"); logger.Append("create");
this._outputFile = new FileStream(this.fileName, FileMode.Create, FileAccess.Write, FileShare.Write, 512, true); this._outputFile = new FileStream(
  this.fileName,
byte[] bom = utf8Encoding.GetPreamble(); FileMode.Create,
  FileAccess.Write,
  FileShare.Read,
  512,
  true
  );
   
  byte[] byteOrderMark = utf8Encoding.GetPreamble();
   
logger.Append(" and writing preamble"); logger.Append(" and writing preamble");
outputFile.Write(bom, 0, bom.Length); this._outputFile.Write(byteOrderMark, 0, byteOrderMark.Length);
} }
   
logger.Append('.'); logger.Append('.');
   
  logger.AppendFormat(" File is {0}opened asynchronously.", this._outputFile.IsAsync ? "" : "not ");
   
logger.Print(); logger.Print();
} }
   
return this._outputFile; return this._outputFile;
} }
} }
   
  public UTF8Encoding utf8Encoding
  {
  get
  {
  if (this._utf8Encoding == null)
  {
  this._utf8Encoding = new UTF8Encoding(true);
  }
   
  return this._utf8Encoding;
  }
  }
   
  #endregion
   
/* /*
* Methods * Methods
* */ * */
public VOID_DataLogger() #region Monobehaviour Lifecycle
{  
this._Name = "CSV Data Logger";  
   
this.stopwatch1_running = false;  
   
this.loggingActive = false;  
this.first_write = true;  
   
this.stopwatch1 = 0;  
this.csv_log_interval_str = "0.5";  
   
this.csvCollectTimer = 0;  
this.outstandingWrites = 0;  
   
this.WindowPos.x = Screen.width - 520;  
this.WindowPos.y = 85;  
}  
   
public override void ModuleWindow(int _)  
{  
GUIStyle txt_white = new GUIStyle(GUI.skin.label);  
txt_white.normal.textColor = txt_white.focused.textColor = Color.white;  
txt_white.alignment = TextAnchor.UpperRight;  
GUIStyle txt_green = new GUIStyle(GUI.skin.label);  
txt_green.normal.textColor = txt_green.focused.textColor = Color.green;  
txt_green.alignment = TextAnchor.UpperRight;  
GUIStyle txt_yellow = new GUIStyle(GUI.skin.label);  
txt_yellow.normal.textColor = txt_yellow.focused.textColor = Color.yellow;  
txt_yellow.alignment = TextAnchor.UpperRight;  
   
GUILayout.BeginVertical();  
   
GUILayout.Label("System time: " + DateTime.Now.ToString("HH:mm:ss"));  
GUILayout.Label(VOID_Tools.ConvertInterval(stopwatch1));  
   
GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));  
if (GUILayout.Button("Start"))  
{  
if (stopwatch1_running == false) stopwatch1_running = true;  
}  
if (GUILayout.Button("Stop"))  
{  
if (stopwatch1_running == true) stopwatch1_running = false;  
}  
if (GUILayout.Button("Reset"))  
{  
if (stopwatch1_running == true) stopwatch1_running = false;  
stopwatch1 = 0;  
}  
GUILayout.EndHorizontal();  
   
GUIStyle label_style = txt_white;  
string log_label = "Inactive";  
if (loggingActive && vessel.situation.ToString() == "PRELAUNCH")  
{  
log_label = "Awaiting launch";  
label_style = txt_yellow;  
}  
if (loggingActive && vessel.situation.ToString() != "PRELAUNCH")  
{  
log_label = "Active";  
label_style = txt_green;  
}  
GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));  
this.loggingActive = GUILayout.Toggle(loggingActive, "Data logging: ", GUILayout.ExpandWidth(false));  
   
GUILayout.Label(log_label, label_style, GUILayout.ExpandWidth(true));  
GUILayout.EndHorizontal();  
   
GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));  
GUILayout.Label("Interval: ", GUILayout.ExpandWidth(false));  
csv_log_interval_str = GUILayout.TextField(csv_log_interval_str, GUILayout.ExpandWidth(true));  
GUILayout.Label("s", GUILayout.ExpandWidth(false));  
GUILayout.EndHorizontal();  
   
float new_log_interval;  
if (float.TryParse(csv_log_interval_str, out new_log_interval))  
{  
csv_log_interval = new_log_interval;  
}  
   
GUILayout.EndVertical();  
GUI.DragWindow();  
}  
   
public void Update() public void Update()
{ {
  if (this.csvBytes != null && this.csvBytes.Count > 0)
  {
  // csvList is not empty, write it
  this.AsyncWriteData();
  }
   
// CSV Logging // CSV Logging
// from ISA MapSat // from ISA MapSat
if (loggingActive) if (loggingActive)
{ {
//data logging is on //data logging is on
//increment timers //increment timers
csvCollectTimer += Time.deltaTime; this.csvCollectTimer += Time.deltaTime;
   
if (csvCollectTimer >= csv_log_interval && vessel.situation != Vessel.Situations.PRELAUNCH) if (this.csvCollectTimer >= this.logInterval)
{ {
//data logging is on, vessel is not prelaunch, and interval has passed //data logging is on, vessel is not prelaunch, and interval has passed
//write a line to the list //write a line to the list
line_to_csvList(); //write to the csv this.CollectLogData();
} }
   
if (csvList.Count > 0)  
{  
// csvList is not empty and interval between writings to file has elapsed  
//write it  
   
// Tools.PostDebugMessage("")  
   
this.AsyncWriteData();  
}  
}  
else  
{  
//data logging is off  
//reset any timers and clear anything from csvList  
csvCollectTimer = 0f;  
if (csvList.Count > 0) csvList.Clear();  
}  
   
if (stopwatch1_running)  
{  
stopwatch1 += Time.deltaTime;  
} }
} }
   
public void FixedUpdate() {} public void FixedUpdate() {}
   
public void OnDestroy() public void OnDestroy()
{ {
Tools.DebugLogger logger = Tools.DebugLogger.New(this); Tools.DebugLogger logger = Tools.DebugLogger.New(this);
   
logger.Append("Destroying..."); logger.Append("Destroying...");
   
if (this.csvList.Count > 0) this.CloseFileIfOpen();
{  
logger.Append(" Writing final data...");  
this.AsyncWriteData();  
}  
   
while (this.outstandingWrites > 0)  
{  
System.Threading.Thread.Sleep(10);  
}  
   
if (this._outputFile != null)  
{  
logger.Append(" Closing File...");  
this.outputFile.Close();  
}  
   
logger.Append(" Done."); logger.Append(" Done.");
logger.Print(); logger.Print(false);
} }
   
~VOID_DataLogger() #endregion
{  
this.OnDestroy(); #region VOID_Module Overrides
}  
  public override void LoadConfig()
protected void AsyncWriteCallback(IAsyncResult result) {
{ base.LoadConfig();
Tools.PostDebugMessage(this, "Got async callback, IsCompleted = {0}", result.IsCompleted);  
  this.logIntervalStr = this.logInterval.value.ToString("#.0##");
this.outputFile.EndWrite(result); }
this.outstandingWrites--;  
} public override void ModuleWindow(int _)
  {
private void AsyncWriteData() GUILayout.BeginVertical();
{  
if (this.utf8Encoding == null) GUILayout.Label(
{ string.Format("System time: {0}", DateTime.Now.ToString("HH:mm:ss")),
this.utf8Encoding = new System.Text.UTF8Encoding(true, true); GUILayout.ExpandWidth(true)
} );
  GUILayout.Label(
List<byte> bytes = new List<byte>(); string.Format("Kerbin time: {0}", VOID_Tools.FormatDate(Planetarium.GetUniversalTime())),
  GUILayout.ExpandWidth(true)
foreach (string line in this.csvList) );
{  
byte[] lineBytes = utf8Encoding.GetBytes(line); GUIStyle activeLabelStyle = VOID_Styles.labelRed;
bytes.AddRange(lineBytes); string activeLabelText = "Inactive";
} if (loggingActive)
  {
WriteState state = new WriteState(); activeLabelText = "Active";
  activeLabelStyle = VOID_Styles.labelGreen;
state.bytes = bytes.ToArray(); }
state.stream = this.outputFile;  
  GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
this.outstandingWrites++;  
var writeCallback = new AsyncCallback(this.AsyncWriteCallback); this.loggingActive = GUILayout.Toggle(loggingActive, "Data logging: ", GUILayout.ExpandWidth(false));
  GUILayout.Label(activeLabelText, activeLabelStyle, GUILayout.ExpandWidth(true));
this.outputFile.BeginWrite(state.bytes, 0, state.bytes.Length, writeCallback, state);  
  GUILayout.EndHorizontal();
this.csvList.Clear();  
} GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
   
private void line_to_csvList() GUILayout.Label("Interval: ", GUILayout.ExpandWidth(false));
{  
  logIntervalStr = GUILayout.TextField(logIntervalStr, GUILayout.ExpandWidth(true));
  GUILayout.Label("s", GUILayout.ExpandWidth(false));
   
  GUILayout.EndHorizontal();
   
  float newLogInterval;
  if (float.TryParse(logIntervalStr, out newLogInterval))
  {
  logInterval.value = newLogInterval;
  this.logIntervalStr = this.logInterval.value.ToString("#.0##");
  }
   
  GUILayout.EndVertical();
   
  GUI.DragWindow();
  }
   
  #endregion
   
  #region Data Collection
   
  private void CollectLogData()
  {
  if (this.csvBytes == null)
  {
  this.csvBytes = new List<byte>();
  }
   
//called if logging is on and interval has passed //called if logging is on and interval has passed
//writes one line to the csvList //writes one line to the csvList
   
StringBuilder line = new StringBuilder(); StringBuilder line = new StringBuilder();
   
if (first_write) if (firstWrite)
{ {
first_write = false; firstWrite = false;
line.Append( line.Append(
  "\"Kerbin Universal Time (s)\"," +
"\"Mission Elapsed Time (s)\t\"," + "\"Mission Elapsed Time (s)\t\"," +
"\"Altitude ASL (m)\"," + "\"Altitude ASL (m)\"," +
"\"Altitude above terrain (m)\"," + "\"Altitude above terrain (m)\"," +
"\"Surface Latitude (°)\"," + "\"Surface Latitude (°)\"," +
"\"Surface Longitude (°)\"," + "\"Surface Longitude (°)\"," +
"\"Orbital Velocity (m/s)\"," + "\"Orbital Velocity (m/s)\"," +
"\"Surface Velocity (m/s)\"," + "\"Surface Velocity (m/s)\"," +
"\"Vertical Speed (m/s)\"," + "\"Vertical Speed (m/s)\"," +
"\"Horizontal Speed (m/s)\"," + "\"Horizontal Speed (m/s)\"," +
"\"Gee Force (gees)\"," + "\"Gee Force (gees)\"," +
"\"Temperature (°C)\"," + "\"Temperature (°C)\"," +
"\"Gravity (m/s²)\"," + "\"Gravity (m/s²)\"," +
"\"Atmosphere Density (g/m³)\"," + "\"Atmosphere Density (g/m³)\"," +
"\"Downrange Distance (m)\"," + "\"Downrange Distance (m)\"," +
"\n" "\n"
); );
} }
   
  // Universal time
  line.Append(Planetarium.GetUniversalTime().ToString("F2"));
  line.Append(',');
   
//Mission time //Mission time
line.Append(vessel.missionTime.ToString("F3")); line.Append(vessel.missionTime.ToString("F3"));
line.Append(','); line.Append(',');
   
//Altitude ASL //Altitude ASL
line.Append(vessel.orbit.altitude.ToString("F3")); line.Append(VOID_Data.orbitAltitude.Value.ToString("F3"));
line.Append(','); line.Append(',');
   
//Altitude (true) //Altitude (true)
double alt_true = vessel.orbit.altitude - vessel.terrainAltitude; line.Append(VOID_Data.trueAltitude.Value.ToString("F3"));
if (vessel.terrainAltitude < 0) alt_true = vessel.orbit.altitude;  
line.Append(alt_true.ToString("F3"));  
line.Append(','); line.Append(',');
   
// Surface Latitude // Surface Latitude
line.Append('"'); line.Append('"');
line.Append(VOID_Data.surfLatitude.Value); line.Append(VOID_Data.surfLatitude.Value);
line.Append('"'); line.Append('"');
line.Append(','); line.Append(',');
   
// Surface Longitude // Surface Longitude
line.Append('"'); line.Append('"');
line.Append(VOID_Data.surfLongitude.Value); line.Append(VOID_Data.surfLongitude.Value);
line.Append('"'); line.Append('"');
line.Append(','); line.Append(',');
   
//Orbital velocity //Orbital velocity
line.Append(vessel.orbit.vel.magnitude.ToString("F3")); line.Append(VOID_Data.orbitVelocity.Value.ToString("F3"));
line.Append(','); line.Append(',');
   
//surface velocity //surface velocity
line.Append(vessel.srf_velocity.magnitude.ToString("F3")); line.Append(VOID_Data.surfVelocity.Value.ToString("F3"));
line.Append(','); line.Append(',');
   
//vertical speed //vertical speed
line.Append(vessel.verticalSpeed.ToString("F3")); line.Append(VOID_Data.vertVelocity.Value.ToString("F3"));
line.Append(','); line.Append(',');
   
//horizontal speed //horizontal speed
line.Append(vessel.horizontalSrfSpeed.ToString("F3")); line.Append(VOID_Data.horzVelocity.Value.ToString("F3"));
line.Append(','); line.Append(',');
   
//gee force //gee force
line.Append(vessel.geeForce.ToString("F3")); line.Append(VOID_Data.geeForce.Value.ToString("F3"));
line.Append(','); line.Append(',');
   
//temperature //temperature
line.Append(vessel.flightIntegrator.getExternalTemperature().ToString("F2")); line.Append(VOID_Data.temperature.Value.ToString("F2"));
line.Append(','); line.Append(',');
   
//gravity //gravity
double r_vessel = vessel.mainBody.Radius + vessel.mainBody.GetAltitude(vessel.findWorldCenterOfMass()); line.Append(VOID_Data.gravityAccel.Value.ToString("F3"));
double g_vessel = (VOID_Core.Constant_G * vessel.mainBody.Mass) / (r_vessel * r_vessel);  
line.Append(g_vessel.ToString("F3"));  
line.Append(','); line.Append(',');
   
//atm density //atm density
line.Append((vessel.atmDensity * 1000).ToString("F3")); line.Append(VOID_Data.atmDensity.Value.ToString("G3"));
line.Append(','); line.Append(',');
   
// Downrange Distance // Downrange Distance
line.Append((VOID_Data.downrangeDistance.Value.ToString("G3"))); line.Append((VOID_Data.downrangeDistance.Value.ToString("G3")));
   
line.Append('\n'); line.Append('\n');
   
csvList.Add(line.ToString()); csvBytes.AddRange(this.utf8Encoding.GetBytes(line.ToString()));
   
csvCollectTimer = 0f; this.csvCollectTimer = 0f;
} }
   
  #endregion
   
  #region File IO Methods
   
  protected void AsyncWriteCallback(IAsyncResult result)
  {
  Tools.PostDebugMessage(this, "Got async callback, IsCompleted = {0}", result.IsCompleted);
   
  this.outputFile.EndWrite(result);
  this.outstandingWrites--;
  }
   
  private void AsyncWriteData()
  {
  WriteState state = new WriteState();
   
  state.bytes = this.csvBytes.ToArray();
  state.stream = this.outputFile;
   
  this.outstandingWrites++;
  var writeCallback = new AsyncCallback(this.AsyncWriteCallback);
   
  this.outputFile.BeginWrite(state.bytes, 0, state.bytes.Length, writeCallback, state);
   
  this.csvBytes.Clear();
  }
   
  private void CloseFileIfOpen()
  {
  Tools.DebugLogger logger = Tools.DebugLogger.New(this);
   
  logger.AppendFormat("Cleaning up file {0}...", this.fileName);
   
  if (this.csvBytes != null && this.csvBytes.Count > 0)
  {
  logger.Append(" Writing remaining data...");
  this.AsyncWriteData();
  }
   
  logger.Append(" Waiting for writes to finish.");
  while (this.outstandingWrites > 0)
  {
  logger.Append('.');
  System.Threading.Thread.Sleep(10);
  }
   
  if (this._outputFile != null)
  {
  this._outputFile.Close();
  this._outputFile = null;
  logger.Append(" File closed.");
  }
   
  logger.Print(false);
  }
   
  #endregion
   
  #region Constructors & Destructors
   
  public VOID_DataLogger()
  {
  this.Name = "CSV Data Logger";
   
  this.loggingActive = false;
  this.firstWrite = true;
   
  this.logInterval = 0.5f;
  this.csvCollectTimer = 0f;
   
  this.outstandingWrites = 0;
   
  this.WindowPos.x = Screen.width - 520f;
  this.WindowPos.y = 85f;
   
  this.core.onApplicationQuit += delegate(object sender)
  {
  this.CloseFileIfOpen();
  };
  }
   
  ~VOID_DataLogger()
  {
  this.OnDestroy();
  }
   
  #endregion
   
  #region Subclasses
   
private class WriteState private class WriteState
{ {
public byte[] bytes; public byte[] bytes;
public FileStream stream; public FileStream stream;
} }
   
  #endregion
} }
} }